git.delta.rocks / jrsonnet / refs/commits / 9b70922a5a83

difftreelog

feat (de)serialize nix imports

upkxyxktYaroslav Bolyukin2026-01-22parent: #3bdc221.patch.diff
in: trunk

5 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2208,6 +2208,7 @@
 name = "nixlike"
 version = "0.1.0"
 dependencies = [
+ "itertools 0.14.0",
  "linked-hash-map",
  "peg",
  "ron",
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -10,6 +10,7 @@
 linked-hash-map = "0.5.6"
 peg = "0.8.5"
 ron = "0.11.0"
-serde = "1.0.219"
+serde = { version = "1.0.219", features = ["derive"] }
 serde-transcode = "1.1.1"
 serde_json = "1.0.140"
+itertools = "0.14.0"
modifiedcrates/nixlike/src/de_impl.rsdiffbeforeafterboth
--- a/crates/nixlike/src/de_impl.rs
+++ b/crates/nixlike/src/de_impl.rs
@@ -2,7 +2,7 @@
 
 use linked_hash_map::LinkedHashMap;
 use serde::{
-	Deserializer,
+	Deserializer, Serialize,
 	de::{self, MapAccess, SeqAccess},
 };
 
@@ -138,6 +138,10 @@
 			Value::Object(o) => visitor.visit_map(ObjectAccess::new(o)),
 			Value::Array(a) => visitor.visit_seq(ArrayAccess::new(a)),
 			Value::Null => visitor.visit_none(),
+			Value::Import(d) => {
+				let value = d.serialize(crate::se_impl::MySerialize)?;
+				value.deserialize_any(visitor)
+			}
 		}
 	}
 
@@ -323,7 +327,13 @@
 	where
 		V: serde::de::Visitor<'de>,
 	{
-		visitor.visit_map(self.parse_object().map(ObjectAccess::new)?)
+		match self {
+			Value::Import(d) => {
+				let value = d.serialize(crate::se_impl::MySerialize)?;
+				value.deserialize_map(visitor)
+			}
+			v => visitor.visit_map(v.parse_object().map(ObjectAccess::new)?),
+		}
 	}
 
 	fn deserialize_struct<V>(
modifiedcrates/nixlike/src/lib.rsdiffbeforeafterboth
before · crates/nixlike/src/lib.rs
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}
modifiedcrates/nixlike/src/to_string.rsdiffbeforeafterboth
--- a/crates/nixlike/src/to_string.rs
+++ b/crates/nixlike/src/to_string.rs
@@ -1,3 +1,5 @@
+use itertools::Itertools;
+
 use crate::Value;
 
 pub fn write_identifier(k: &str, out: &mut String) {
@@ -76,6 +78,10 @@
 	}
 }
 
+fn write_nix_import(import: &str, out: &mut String, padding: &mut usize) {
+	out.push_str("import ");
+	write_nix_str(import, out, padding)
+}
 fn write_nix_buf(value: &Value, out: &mut String, padding: &mut usize) {
 	match value {
 		Value::Null => out.push_str("null"),
@@ -98,9 +104,17 @@
 				out.push(']');
 			}
 		}
+		Value::Import(i) => write_nix_import(&i.import, out, padding),
 		Value::Object(obj) => {
 			if obj.is_empty() {
-				out.push_str("{ }")
+				out.push_str("{ }");
+			} else if obj.len() == 2
+				&& let Some([(importk, Value::String(importv)), (markerk, Value::Null)]) =
+					obj.iter().next_array::<2>()
+				&& markerk == "__magic_marker"
+				&& importk == "__magic_import"
+			{
+				write_nix_import(importv, out, padding)
 			} else {
 				out.push_str("{\n");
 				*padding += 1;