git.delta.rocks / jrsonnet / refs/commits / 3bdc221f14ae

difftreelog

source

crates/nixlike/src/lib.rs6.1 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 linked_hash_map::LinkedHashMap;9use peg::str::LineCol;10use se_impl::MySerialize;11use serde::{Deserialize, Serialize};1213mod de_impl;14mod se_impl;15mod to_string;1617pub use to_string::escape_string;1819#[derive(thiserror::Error, Debug)]20pub enum Error {21	#[error("bad number")]22	BadNumber,23	#[error("expected {0}")]24	Expected(&'static str),25	#[error("parse error")]26	ParseError(#[from] peg::error::ParseError<LineCol>),27	#[error("{0}")]28	Custom(String),29	#[error("io: {0}")]30	Io(#[from] std::io::Error),31	#[error("fmt: {0}")]32	Fmt(#[from] std::fmt::Error),33}3435#[derive(Debug)]36pub enum Value {37	Number(i64),38	String(String),39	Boolean(bool),40	Object(LinkedHashMap<String, Value>),41	Array(Vec<Value>),42	Null,43}4445fn count_spaces(l: &str) -> usize {46	l.chars().take_while(|&c| c == ' ').count()47}48fn is_significant(l: &str) -> bool {49	count_spaces(l) != l.len()50}5152fn dedent(l: &str, by: usize) -> &str {53	assert!(54		l[0..by.min(l.len())].chars().all(|c| c == ' '),55		"dedent calculation is wrong"56	);57	&l[by.min(l.len())..]58}5960fn process_multiline(lines: Vec<&str>) -> String {61	// Even when parsing '''', there is single "line" between those '' delimiters.62	// unwrap_or is for case where there is no significant lines63	let dedent_by = lines64		.iter()65		.copied()66		.filter(|c| is_significant(c))67		.map(count_spaces)68		.min()69		.unwrap_or(0);7071	let mut out = String::new();7273	let mut had_first = false;74	for (i, line) in lines.into_iter().enumerate() {75		// Newline after '' is ignored, if there is no text.76		if i == 0 && !is_significant(line) {77			continue;78		}79		if had_first {80			out.push('\n');81		}82		had_first = true;83		// ''' is hard escape84		for (i, part) in dedent(line, dedent_by).split("'''").enumerate() {85			if i != 0 {86				out.push_str(r#"""""#);87			}88			// This is the only replacements done by nixlike writer, no need to support more.89			out.push_str(&part.replace("''${", "${").replace("''\\t", "\t"));90		}91	}9293	out94}9596peg::parser! {97pub grammar nixlike() for str {98	rule number() -> i6499		= quiet! { v:$(['0'..='9' | '+' | '-']+) {? v.parse().map_err(|_| "<number>")} } / expected!("<number>")100	rule string_char() -> &'input str101		= "\\\"" { "\"" }102		/ "\\\\" { "\\" }103		/ "\\n" { "\n" }104		/ "\\t" { "\t" }105		/ "\\r" { "\r" }106		/ "\\$" { "$" }107		/ c:$([_]) { c }108	rule string() -> String = singleline_string() / multiline_string();109	rule singleline_string() -> String110		= quiet! { "\"" v:(!"\"" c:string_char() {c})* "\"" { v.into_iter().collect() } } / expected!("<string>")111	pub rule multiline_string() -> String112		= "''"113		// 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...114		// This logic is complicated, see `parse_multiline` test.115		lines:$(("'''" / !"''" [_])*) "''"116		{117			process_multiline(lines.split('\n').collect())118		}119	rule boolean() -> bool120		= quiet! { "true" {true}121		/ "false" {false} } / expected!("<boolean>")122	rule indent() -> String123		= quiet! {124			s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-']+) { s.to_owned() }125			/ "\"" s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) "\"" { s.to_owned() }126		} / expected!("<identifier>")127	rule object() -> LinkedHashMap<String, Value>128		= "{" _129			e:(k:indent()++(_ "." _) _ "=" _ v:value() _ ";" _ {(k, v)})*130		"}" {?131			let mut out = LinkedHashMap::new();132			for (k, v) in e {133				let mut map = &mut out;134				for v in k.iter().take(k.len() - 1) {135					map = match map.entry(v.clone()).or_insert_with(|| Value::Object(Default::default())) {136						Value::Object(v) => v,137						_ => return Err("expected object"),138					}139				}140141				let key = k.into_iter().next_back().unwrap();142				if map.contains_key(&key) {143					return Err("can't override object");144				}145				map.insert(key, v);146			}147			Ok(out)148		}149150	rule array() -> Vec<Value>151		= "[" _ v:value()**_ _ "]" {v}152153	rule value() -> Value154		= o:object() { Value::Object(o) }155		/ a:array() { Value::Array(a) }156		/ s:string() { Value::String(s) }157		/ "null" { Value::Null }158		/ b:boolean() { Value::Boolean(b) }159		/ n:number() { Value::Number(n) }160161	pub rule root() -> Value162		= _ v:value() _ { v }163164	rule _()165		= ( quiet!{ [' ' | '\t' | '\n']+ }166		/ "#" (!['\n'] [_])* "\n" )*167}168}169170pub fn parse_str<'de, D: Deserialize<'de>>(s: &str) -> Result<D, Error> {171	let value = nixlike::root(s)?;172	D::deserialize(value)173}174175pub fn parse_value<'de, D: Deserialize<'de>>(value: Value) -> Result<D, Error> {176	D::deserialize(value)177}178179pub fn serialize_value_pretty(value: Value) -> String {180	to_string::write_nix(&value)181}182183pub fn serialize<S: Serialize>(value: S) -> Result<String, Error> {184	let value: Value = value.serialize(MySerialize)?;185	Ok(serialize_value_pretty(value))186}187188pub fn format_identifier(i: &str) -> String {189	let mut out = String::new();190	to_string::write_identifier(i, &mut out);191	out192}193194#[test]195fn test() {196	assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"\n");197}198pub fn format_nix(value: &String) -> String {199	// TODO200	value.to_owned()201}202203#[test]204fn parse_multiline() {205	// First line is ignored, unless there is a significant characters.206	assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), "");207	// Rest of the lines are processed normally.208	assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n");209	// Example with significant character on first line.210	assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n");211	// There might be nothing in multiline string block.212	assert_eq!(nixlike::multiline_string("''''").expect("parse"), "");213	// And there also might just be spaces, they are removed due to dedent, and output is empty because214	// first line was also ignored due to missing significant characters.215	assert_eq!(nixlike::multiline_string("''    ''").expect("parse"), "");216}