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

difftreelog

source

crates/nixlike/src/lib.rs5.5 KiBsourcehistory
1//! Serialization/deserialization for nix subset usable for static configurations2//! Serialized results from this library are readable by both this library and standard nix tools34use linked_hash_map::LinkedHashMap;5use peg::str::LineCol;6use se_impl::MySerialize;7use serde::{Deserialize, Serialize};89mod de_impl;10mod se_impl;11mod to_string;1213pub use to_string::escape_string;1415#[derive(thiserror::Error, Debug)]16pub enum Error {17	#[error("bad number")]18	BadNumber,19	#[error("expected {0}")]20	Expected(&'static str),21	#[error("parse error")]22	ParseError(#[from] peg::error::ParseError<LineCol>),23	#[error("{0}")]24	Custom(String),25	#[error("io: {0}")]26	Io(#[from] std::io::Error),27	#[error("fmt: {0}")]28	Fmt(#[from] std::fmt::Error),29}3031#[derive(Debug)]32pub enum Value {33	Number(i64),34	String(String),35	Boolean(bool),36	Object(LinkedHashMap<String, Value>),37	Array(Vec<Value>),38	Null,39}4041fn count_spaces(l: &str) -> usize {42	l.chars().take_while(|&c| c == ' ').count()43}44fn is_significant(l: &str) -> bool {45	count_spaces(l) != l.len()46}4748fn dedent(l: &str, by: usize) -> &str {49	assert!(50		l[0..by.min(l.len())].chars().all(|c| c == ' '),51		"dedent calculation is wrong"52	);53	&l[by.min(l.len())..]54}5556fn process_multiline(lines: Vec<&str>) -> String {57	// Even when parsing '''', there is single "line" between those '' delimiters.58	// unwrap_or is for case where there is no significant lines59	let dedent_by = lines60		.iter()61		.copied()62		.filter(|c| is_significant(c))63		.map(count_spaces)64		.min()65		.unwrap_or(0);6667	let mut out = String::new();6869	let mut had_first = false;70	for (i, line) in lines.into_iter().enumerate() {71		// Newline after '' is ignored, if there is no text.72		if i == 0 && !is_significant(line) {73			continue;74		}75		if had_first {76			out.push('\n');77		}78		had_first = true;79		// ''' is hard escape80		for (i, part) in dedent(line, dedent_by).split("'''").enumerate() {81			if i != 0 {82				out.push_str(r#"""""#);83			}84			// This is the only replacements done by nixlike writer, no need to support more.85			out.push_str(&part.replace("''${", "${").replace("''\\t", "\t"));86		}87	}8889	out90}9192peg::parser! {93pub grammar nixlike() for str {94	rule number() -> i6495		= quiet! { v:$(['0'..='9' | '+' | '-']+) {? v.parse().map_err(|_| "<number>")} } / expected!("<number>")96	rule string_char() -> &'input str97		= "\\\"" { "\"" }98		/ "\\\\" { "\\" }99		/ "\\n" { "\n" }100		/ "\\t" { "\t" }101		/ "\\r" { "\r" }102		/ "\\$" { "$" }103		/ c:$([_]) { c }104	rule string() -> String = singleline_string() / multiline_string();105	rule singleline_string() -> String106		= quiet! { "\"" v:(!"\"" c:string_char() {c})* "\"" { v.into_iter().collect() } } / expected!("<string>")107	pub rule multiline_string() -> String108		= "''"109		// 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...110		// This logic is complicated, see `parse_multiline` test.111		lines:$(("'''" / !"''" [_])*) "''"112		{113			process_multiline(lines.split('\n').collect())114		}115	rule boolean() -> bool116		= quiet! { "true" {true}117		/ "false" {false} } / expected!("<boolean>")118	rule indent() -> String119		= quiet! {120			s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-']+) { s.to_owned() }121			/ "\"" s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) "\"" { s.to_owned() }122		} / expected!("<identifier>")123	rule object() -> LinkedHashMap<String, Value>124		= "{" _125			e:(k:indent()++(_ "." _) _ "=" _ v:value() _ ";" _ {(k, v)})*126		"}" {?127			let mut out = LinkedHashMap::new();128			for (k, v) in e {129				let mut map = &mut out;130				for v in k.iter().take(k.len() - 1) {131					map = match map.entry(v.clone()).or_insert_with(|| Value::Object(Default::default())) {132						Value::Object(v) => v,133						_ => return Err("expected object"),134					}135				}136137				let key = k.into_iter().last().unwrap();138				if map.contains_key(&key) {139					return Err("can't override object");140				}141				map.insert(key, v);142			}143			Ok(out)144		}145146	rule array() -> Vec<Value>147		= "[" _ v:value()**_ _ "]" {v}148149	rule value() -> Value150		= o:object() { Value::Object(o) }151		/ a:array() { Value::Array(a) }152		/ s:string() { Value::String(s) }153		/ "null" { Value::Null }154		/ b:boolean() { Value::Boolean(b) }155		/ n:number() { Value::Number(n) }156157	pub rule root() -> Value158		= _ v:value() _ { v }159160	rule _()161		= ( quiet!{ [' ' | '\t' | '\n']+ }162		/ "#" (!['\n'] [_])* "\n" )*163}164}165166pub fn parse_str<'de, D: Deserialize<'de>>(s: &str) -> Result<D, Error> {167	let value = nixlike::root(s)?;168	D::deserialize(value)169}170171pub fn parse_value<'de, D: Deserialize<'de>>(value: Value) -> Result<D, Error> {172	D::deserialize(value)173}174175pub fn serialize_value_pretty(value: Value) -> String {176	to_string::write_nix(&value)177}178179pub fn serialize<S: Serialize>(value: S) -> Result<String, Error> {180	let value: Value = value.serialize(MySerialize)?;181	Ok(serialize_value_pretty(value))182}183184pub fn format_identifier(i: &str) -> String {185	let mut out = String::new();186	to_string::write_identifier(i, &mut out);187	out188}189190#[test]191fn test() {192	assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"\n");193}194pub fn format_nix(value: &String) -> String {195	let (_, out) = alejandra::format::in_memory("".to_owned(), value.to_owned());196	out197}198199#[test]200fn parse_multiline() {201	assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), "");202	assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n");203	assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n");204	assert_eq!(nixlike::multiline_string("''''").expect("parse"), "");205	assert_eq!(nixlike::multiline_string("''    ''").expect("parse"), "");206}