git.delta.rocks / jrsonnet / refs/heads / trunk

difftreelog

source

crates/nixlike/src/lib.rs7.0 KiBsourcehistory
1//! Serialization/deserialization for nix subset usable for static configurations2//!3//! Serialized results from this library are readable by both this library and standard nix tools.4//! Nix produced output should also be readable by this library, however, you can't write arbitrary nix5//! expressions and expect it to work, only basic primitives are supported, and there is no6//! variables/recursive records, interpolation, e.t.c.78use std::marker::PhantomData;910use linked_hash_map::LinkedHashMap;11use peg::str::LineCol;12use se_impl::MySerialize;13use serde::{Deserialize, Serialize};1415mod de_impl;16mod se_impl;17mod to_string;1819pub use to_string::escape_string;2021#[derive(thiserror::Error, Debug)]22pub enum Error {23	#[error("bad number")]24	BadNumber,25	#[error("expected {0}")]26	Expected(&'static str),27	#[error("parse error")]28	ParseError(#[from] peg::error::ParseError<LineCol>),29	#[error("{0}")]30	Custom(String),31	#[error("io: {0}")]32	Io(#[from] std::io::Error),33	#[error("fmt: {0}")]34	Fmt(#[from] std::fmt::Error),35}3637#[derive(Debug)]38pub enum Value {39	Number(i64),40	String(String),41	Boolean(bool),42	Object(LinkedHashMap<String, Value>),43	Array(Vec<Value>),44	Import(NixImport),45	Null,46}4748#[derive(Debug, Serialize, Deserialize)]49pub struct NixImport {50	#[serde(rename = "__magic_import")]51	import: String,52	// Magic values should have exactly two values to avoid pretty-printing53	// as nix inline object value54	__magic_marker: PhantomData<()>,55}5657impl NixImport {58	pub fn new(import: impl AsRef<str>) -> Self {59		Self {60			import: import.as_ref().to_string(),61			__magic_marker: PhantomData,62		}63	}64}6566fn count_spaces(l: &str) -> usize {67	l.chars().take_while(|&c| c == ' ').count()68}69fn is_significant(l: &str) -> bool {70	count_spaces(l) != l.len()71}7273fn dedent(l: &str, by: usize) -> &str {74	assert!(75		l[0..by.min(l.len())].chars().all(|c| c == ' '),76		"dedent calculation is wrong"77	);78	&l[by.min(l.len())..]79}8081fn process_multiline(lines: Vec<&str>) -> String {82	// Even when parsing '''', there is single "line" between those '' delimiters.83	// unwrap_or is for case where there is no significant lines84	let dedent_by = lines85		.iter()86		.copied()87		.filter(|c| is_significant(c))88		.map(count_spaces)89		.min()90		.unwrap_or(0);9192	let mut out = String::new();9394	let mut had_first = false;95	for (i, line) in lines.into_iter().enumerate() {96		// Newline after '' is ignored, if there is no text.97		if i == 0 && !is_significant(line) {98			continue;99		}100		if had_first {101			out.push('\n');102		}103		had_first = true;104		// ''' is hard escape105		for (i, part) in dedent(line, dedent_by).split("'''").enumerate() {106			if i != 0 {107				out.push_str(r#"""""#);108			}109			// This is the only replacements done by nixlike writer, no need to support more.110			out.push_str(&part.replace("''${", "${").replace("''\\t", "\t"));111		}112	}113114	out115}116117peg::parser! {118pub grammar nixlike() for str {119	rule number() -> i64120		= quiet! { v:$(['0'..='9' | '+' | '-']+) {? v.parse().map_err(|_| "<number>")} } / expected!("<number>")121	rule string_char() -> &'input str122		= "\\\"" { "\"" }123		/ "\\\\" { "\\" }124		/ "\\n" { "\n" }125		/ "\\t" { "\t" }126		/ "\\r" { "\r" }127		/ "\\$" { "$" }128		/ c:$([_]) { c }129	rule string() -> String = singleline_string() / multiline_string();130	rule singleline_string() -> String131		= quiet! { "\"" v:(!"\"" c:string_char() {c})* "\"" { v.into_iter().collect() } } / expected!("<string>")132	pub rule multiline_string() -> String133		= "''"134		// First line may also contain text, and whitespace for it is counted, but if it is empty - then it is'nt counted as full line...135		// This logic is complicated, see `parse_multiline` test.136		lines:$(("'''" / !"''" [_])*) "''"137		{138			process_multiline(lines.split('\n').collect())139		}140	rule boolean() -> bool141		= quiet! { "true" {true}142		/ "false" {false} } / expected!("<boolean>")143	rule indent() -> String144		= quiet! {145			s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-']+) { s.to_owned() }146			/ "\"" s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) "\"" { s.to_owned() }147		} / expected!("<identifier>")148	rule object() -> LinkedHashMap<String, Value>149		= "{" _150			e:(k:indent()++(_ "." _) _ "=" _ v:value() _ ";" _ {(k, v)})*151		"}" {?152			let mut out = LinkedHashMap::new();153			for (k, v) in e {154				let mut map = &mut out;155				for v in k.iter().take(k.len() - 1) {156					map = match map.entry(v.clone()).or_insert_with(|| Value::Object(Default::default())) {157						Value::Object(v) => v,158						_ => return Err("expected object"),159					}160				}161162				let key = k.into_iter().next_back().unwrap();163				if map.contains_key(&key) {164					return Err("can't override object");165				}166				map.insert(key, v);167			}168			Ok(out)169		}170171	rule array() -> Vec<Value>172		= "[" _ v:value()**_ _ "]" {v}173174	rule import() -> NixImport175		= "import" _ s:string() {NixImport::new(s)}176177	rule value() -> Value178		= i:import() { Value::Import(i) }179		/ o:object() { Value::Object(o) }180		/ a:array() { Value::Array(a) }181		/ s:string() { Value::String(s) }182		/ "null" { Value::Null }183		/ b:boolean() { Value::Boolean(b) }184		/ n:number() { Value::Number(n) }185186	pub rule root() -> Value187		= _ v:value() _ { v }188189	rule _()190		= ( quiet!{ [' ' | '\t' | '\n']+ }191		/ "#" (!['\n'] [_])* "\n" )*192}193}194195pub fn parse_str<'de, D: Deserialize<'de>>(s: &str) -> Result<D, Error> {196	let value = nixlike::root(s)?;197	D::deserialize(value)198}199200pub fn parse_value<'de, D: Deserialize<'de>>(value: Value) -> Result<D, Error> {201	D::deserialize(value)202}203204pub fn serialize_value_pretty(value: Value) -> String {205	to_string::write_nix(&value)206}207208pub fn serialize<S: Serialize>(value: S) -> Result<String, Error> {209	let value: Value = value.serialize(MySerialize)?;210	Ok(serialize_value_pretty(value))211}212213pub fn format_identifier(i: &str) -> String {214	let mut out = String::new();215	to_string::write_identifier(i, &mut out);216	out217}218219pub fn format_nix(value: &String) -> String {220	// TODO221	value.to_owned()222}223224#[cfg(test)]225mod tests {226	use super::*;227228	#[test]229	fn test() {230		assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"");231	}232233	#[test]234	fn parse_multiline() {235		// First line is ignored, unless there is a significant characters.236		assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), "");237		// Rest of the lines are processed normally.238		assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n");239		// Example with significant character on first line.240		assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n");241		// There might be nothing in multiline string block.242		assert_eq!(nixlike::multiline_string("''''").expect("parse"), "");243		// And there also might just be spaces, they are removed due to dedent, and output is empty because244		// first line was also ignored due to missing significant characters.245		assert_eq!(nixlike::multiline_string("''    ''").expect("parse"), "");246	}247248	#[test]249	fn test_nix_import_roundtrip() {250		let import = NixImport::new("./some/path.nix");251252		let serialized = serialize(&import).expect("serialize");253		assert_eq!(serialized, "import \"./some/path.nix\"");254255		let deserialized: NixImport = parse_str(&serialized).expect("deserialize");256		assert_eq!(deserialized.import, "./some/path.nix");257	}258}