From 9b70922a5a835068077f0d9bf54139a7f6337515 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 06 Jan 2026 00:03:52 +0000 Subject: [PATCH] feat: (de)serialize nix imports --- --- 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", --- 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" --- 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( --- a/crates/nixlike/src/lib.rs +++ b/crates/nixlike/src/lib.rs @@ -5,6 +5,8 @@ //! expressions and expect it to work, only basic primitives are supported, and there is no //! variables/recursive records, interpolation, e.t.c. +use std::marker::PhantomData; + use linked_hash_map::LinkedHashMap; use peg::str::LineCol; use se_impl::MySerialize; @@ -39,9 +41,28 @@ Boolean(bool), Object(LinkedHashMap), Array(Vec), + Import(NixImport), Null, } +#[derive(Debug, Serialize, Deserialize)] +pub struct NixImport { + #[serde(rename = "__magic_import")] + import: String, + // Magic values should have exactly two values to avoid pretty-printing + // as nix inline object value + __magic_marker: PhantomData<()>, +} + +impl NixImport { + pub fn new(import: impl AsRef) -> Self { + Self { + import: import.as_ref().to_string(), + __magic_marker: PhantomData, + } + } +} + fn count_spaces(l: &str) -> usize { l.chars().take_while(|&c| c == ' ').count() } @@ -150,8 +171,12 @@ rule array() -> Vec = "[" _ v:value()**_ _ "]" {v} + rule import() -> NixImport + = "import" _ s:string() {NixImport::new(s)} + rule value() -> Value - = o:object() { Value::Object(o) } + = i:import() { Value::Import(i) } + / o:object() { Value::Object(o) } / a:array() { Value::Array(a) } / s:string() { Value::String(s) } / "null" { Value::Null } @@ -191,26 +216,43 @@ out } -#[test] -fn test() { - assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"\n"); -} pub fn format_nix(value: &String) -> String { // TODO value.to_owned() } -#[test] -fn parse_multiline() { - // First line is ignored, unless there is a significant characters. - assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), ""); - // Rest of the lines are processed normally. - assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n"); - // Example with significant character on first line. - assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n"); - // There might be nothing in multiline string block. - assert_eq!(nixlike::multiline_string("''''").expect("parse"), ""); - // And there also might just be spaces, they are removed due to dedent, and output is empty because - // first line was also ignored due to missing significant characters. - assert_eq!(nixlike::multiline_string("'' ''").expect("parse"), ""); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\""); + } + + #[test] + fn parse_multiline() { + // First line is ignored, unless there is a significant characters. + assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), ""); + // Rest of the lines are processed normally. + assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n"); + // Example with significant character on first line. + assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n"); + // There might be nothing in multiline string block. + assert_eq!(nixlike::multiline_string("''''").expect("parse"), ""); + // And there also might just be spaces, they are removed due to dedent, and output is empty because + // first line was also ignored due to missing significant characters. + assert_eq!(nixlike::multiline_string("'' ''").expect("parse"), ""); + } + + #[test] + fn test_nix_import_roundtrip() { + let import = NixImport::new("./some/path.nix"); + + let serialized = serialize(&import).expect("serialize"); + assert_eq!(serialized, "import \"./some/path.nix\""); + + let deserialized: NixImport = parse_str(&serialized).expect("deserialize"); + assert_eq!(deserialized.import, "./some/path.nix"); + } } --- 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; -- gitstuff