git.delta.rocks / jrsonnet / refs/commits / cd7c759361f6

difftreelog

source

crates/nixlike/src/lib.rs6.2 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 alejandra::config::Indentation;9use linked_hash_map::LinkedHashMap;10use peg::str::LineCol;11use se_impl::MySerialize;12use serde::{Deserialize, Serialize};1314mod de_impl;15mod se_impl;16mod to_string;1718pub use to_string::escape_string;1920#[derive(thiserror::Error, Debug)]21pub enum Error {22	#[error("bad number")]23	BadNumber,24	#[error("expected {0}")]25	Expected(&'static str),26	#[error("parse error")]27	ParseError(#[from] peg::error::ParseError<LineCol>),28	#[error("{0}")]29	Custom(String),30	#[error("io: {0}")]31	Io(#[from] std::io::Error),32	#[error("fmt: {0}")]33	Fmt(#[from] std::fmt::Error),34}3536#[derive(Debug)]37pub enum Value {38	Number(i64),39	String(String),40	Boolean(bool),41	Object(LinkedHashMap<String, Value>),42	Array(Vec<Value>),43	Null,44}4546fn count_spaces(l: &str) -> usize {47	l.chars().take_while(|&c| c == ' ').count()48}49fn is_significant(l: &str) -> bool {50	count_spaces(l) != l.len()51}5253fn dedent(l: &str, by: usize) -> &str {54	assert!(55		l[0..by.min(l.len())].chars().all(|c| c == ' '),56		"dedent calculation is wrong"57	);58	&l[by.min(l.len())..]59}6061fn process_multiline(lines: Vec<&str>) -> String {62	// Even when parsing '''', there is single "line" between those '' delimiters.63	// unwrap_or is for case where there is no significant lines64	let dedent_by = lines65		.iter()66		.copied()67		.filter(|c| is_significant(c))68		.map(count_spaces)69		.min()70		.unwrap_or(0);7172	let mut out = String::new();7374	let mut had_first = false;75	for (i, line) in lines.into_iter().enumerate() {76		// Newline after '' is ignored, if there is no text.77		if i == 0 && !is_significant(line) {78			continue;79		}80		if had_first {81			out.push('\n');82		}83		had_first = true;84		// ''' is hard escape85		for (i, part) in dedent(line, dedent_by).split("'''").enumerate() {86			if i != 0 {87				out.push_str(r#"""""#);88			}89			// This is the only replacements done by nixlike writer, no need to support more.90			out.push_str(&part.replace("''${", "${").replace("''\\t", "\t"));91		}92	}9394	out95}9697peg::parser! {98pub grammar nixlike() for str {99	rule number() -> i64100		= quiet! { v:$(['0'..='9' | '+' | '-']+) {? v.parse().map_err(|_| "<number>")} } / expected!("<number>")101	rule string_char() -> &'input str102		= "\\\"" { "\"" }103		/ "\\\\" { "\\" }104		/ "\\n" { "\n" }105		/ "\\t" { "\t" }106		/ "\\r" { "\r" }107		/ "\\$" { "$" }108		/ c:$([_]) { c }109	rule string() -> String = singleline_string() / multiline_string();110	rule singleline_string() -> String111		= quiet! { "\"" v:(!"\"" c:string_char() {c})* "\"" { v.into_iter().collect() } } / expected!("<string>")112	pub rule multiline_string() -> String113		= "''"114		// 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...115		// This logic is complicated, see `parse_multiline` test.116		lines:$(("'''" / !"''" [_])*) "''"117		{118			process_multiline(lines.split('\n').collect())119		}120	rule boolean() -> bool121		= quiet! { "true" {true}122		/ "false" {false} } / expected!("<boolean>")123	rule indent() -> String124		= quiet! {125			s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-']+) { s.to_owned() }126			/ "\"" s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) "\"" { s.to_owned() }127		} / expected!("<identifier>")128	rule object() -> LinkedHashMap<String, Value>129		= "{" _130			e:(k:indent()++(_ "." _) _ "=" _ v:value() _ ";" _ {(k, v)})*131		"}" {?132			let mut out = LinkedHashMap::new();133			for (k, v) in e {134				let mut map = &mut out;135				for v in k.iter().take(k.len() - 1) {136					map = match map.entry(v.clone()).or_insert_with(|| Value::Object(Default::default())) {137						Value::Object(v) => v,138						_ => return Err("expected object"),139					}140				}141142				let key = k.into_iter().next_back().unwrap();143				if map.contains_key(&key) {144					return Err("can't override object");145				}146				map.insert(key, v);147			}148			Ok(out)149		}150151	rule array() -> Vec<Value>152		= "[" _ v:value()**_ _ "]" {v}153154	rule value() -> Value155		= o:object() { Value::Object(o) }156		/ a:array() { Value::Array(a) }157		/ s:string() { Value::String(s) }158		/ "null" { Value::Null }159		/ b:boolean() { Value::Boolean(b) }160		/ n:number() { Value::Number(n) }161162	pub rule root() -> Value163		= _ v:value() _ { v }164165	rule _()166		= ( quiet!{ [' ' | '\t' | '\n']+ }167		/ "#" (!['\n'] [_])* "\n" )*168}169}170171pub fn parse_str<'de, D: Deserialize<'de>>(s: &str) -> Result<D, Error> {172	let value = nixlike::root(s)?;173	D::deserialize(value)174}175176pub fn parse_value<'de, D: Deserialize<'de>>(value: Value) -> Result<D, Error> {177	D::deserialize(value)178}179180pub fn serialize_value_pretty(value: Value) -> String {181	to_string::write_nix(&value)182}183184pub fn serialize<S: Serialize>(value: S) -> Result<String, Error> {185	let value: Value = value.serialize(MySerialize)?;186	Ok(serialize_value_pretty(value))187}188189pub fn format_identifier(i: &str) -> String {190	let mut out = String::new();191	to_string::write_identifier(i, &mut out);192	out193}194195#[test]196fn test() {197	assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"\n");198}199pub fn format_nix(value: &String) -> String {200	let (_, out) = alejandra::format::in_memory(201		"".to_owned(),202		value.to_owned(),203		alejandra::config::Config {204			indentation: Indentation::TwoSpaces,205		},206	);207	out208}209210#[test]211fn parse_multiline() {212	// First line is ignored, unless there is a significant characters.213	assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), "");214	// Rest of the lines are processed normally.215	assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n");216	// Example with significant character on first line.217	assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n");218	// There might be nothing in multiline string block.219	assert_eq!(nixlike::multiline_string("''''").expect("parse"), "");220	// And there also might just be spaces, they are removed due to dedent, and output is empty because221	// first line was also ignored due to missing significant characters.222	assert_eq!(nixlike::multiline_string("''    ''").expect("parse"), "");223}