use std::collections::BTreeMap;
use std::fmt::{self, Display, Formatter, Write};
type IndexMap<K, V> = BTreeMap<K, V>;
#[derive(Debug, Clone, PartialEq)]
pub enum NixValue {
Null,
Bool(bool),
Int(i64),
Str(String),
MultilineStr(String),
Path(String),
Ident(String),
AttrPath(Vec<String>),
List(Vec<NixValue>),
AttrSet(IndexMap<String, NixValue>),
RecAttrSet(IndexMap<String, NixValue>),
Let {
bindings: IndexMap<String, NixValue>,
body: Box<NixValue>,
},
With { scope: Box<NixValue>, body: Box<NixValue> },
If {
cond: Box<NixValue>,
then_branch: Box<NixValue>,
else_branch: Box<NixValue>,
},
Lambda { param: LambdaParam, body: Box<NixValue> },
Apply { func: Box<NixValue>, arg: Box<NixValue> },
BinOp {
op: String,
lhs: Box<NixValue>,
rhs: Box<NixValue>,
},
HasAttr { set: Box<NixValue>, attr: String },
AttrOr {
set: Box<NixValue>,
attr: String,
fallback: Box<NixValue>,
},
Spread,
Inherit { scope: Option<Box<NixValue>>, names: Vec<String> },
Interpolated(Vec<InterpSegment>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum InterpSegment {
Lit(String),
Expr(NixValue),
}
#[derive(Debug, Clone, PartialEq)]
pub enum LambdaParam {
Single(String),
Set {
fields: Vec<(String, Option<NixValue>)>,
wildcard: bool,
bind_all: Option<String>,
},
}
impl NixValue {
#[must_use]
pub fn s(v: impl Into<String>) -> Self {
Self::Str(v.into())
}
#[must_use]
pub fn ident(v: impl Into<String>) -> Self {
Self::Ident(v.into())
}
#[must_use]
pub fn path(segments: &[&str]) -> Self {
Self::AttrPath(segments.iter().map(|s| (*s).to_string()).collect())
}
#[must_use]
pub fn attrset() -> AttrSetBuilder {
AttrSetBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct AttrSetBuilder {
fields: IndexMap<String, NixValue>,
}
impl AttrSetBuilder {
#[must_use]
pub fn set(mut self, k: impl Into<String>, v: NixValue) -> Self {
self.fields.insert(k.into(), v);
self
}
#[must_use]
pub fn build(self) -> NixValue {
NixValue::AttrSet(self.fields)
}
}
impl Display for NixValue {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write_value(self, f, 0)
}
}
fn write_value(v: &NixValue, f: &mut Formatter<'_>, depth: usize) -> fmt::Result {
match v {
NixValue::Null => f.write_str("null"),
NixValue::Bool(true) => f.write_str("true"),
NixValue::Bool(false) => f.write_str("false"),
NixValue::Int(n) => write!(f, "{n}"),
NixValue::Str(s) => write_nix_string(s, f),
NixValue::MultilineStr(s) => {
f.write_str("''\n")?;
for line in s.lines() {
indent(f, depth + 1)?;
f.write_str(line)?;
f.write_char('\n')?;
}
indent(f, depth)?;
f.write_str("''")
}
NixValue::Path(p) => f.write_str(p),
NixValue::Ident(i) => f.write_str(i),
NixValue::AttrPath(parts) => {
for (i, p) in parts.iter().enumerate() {
if i > 0 {
f.write_char('.')?;
}
f.write_str(p)?;
}
Ok(())
}
NixValue::List(items) => {
if items.is_empty() {
f.write_str("[ ]")
} else {
f.write_str("[\n")?;
for item in items {
indent(f, depth + 1)?;
write_value(item, f, depth + 1)?;
f.write_char('\n')?;
}
indent(f, depth)?;
f.write_char(']')
}
}
NixValue::AttrSet(set) => write_attrset(set, false, f, depth),
NixValue::RecAttrSet(set) => {
f.write_str("rec ")?;
write_attrset(set, false, f, depth)
}
NixValue::Let { bindings, body } => {
f.write_str("let\n")?;
for (k, v) in bindings {
indent(f, depth + 1)?;
f.write_str(k)?;
f.write_str(" = ")?;
write_value(v, f, depth + 1)?;
f.write_str(";\n")?;
}
indent(f, depth)?;
f.write_str("in\n")?;
indent(f, depth)?;
write_value(body, f, depth)
}
NixValue::With { scope, body } => {
f.write_str("with ")?;
write_value(scope, f, depth)?;
f.write_str("; ")?;
write_value(body, f, depth)
}
NixValue::If { cond, then_branch, else_branch } => {
f.write_str("if ")?;
write_value(cond, f, depth)?;
f.write_str(" then ")?;
write_value(then_branch, f, depth)?;
f.write_str(" else ")?;
write_value(else_branch, f, depth)
}
NixValue::Lambda { param, body } => {
write_lambda_param(param, f, depth)?;
f.write_str(": ")?;
write_value(body, f, depth)
}
NixValue::Apply { func, arg } => {
write_value(func, f, depth)?;
f.write_char(' ')?;
let needs_parens = matches!(
arg.as_ref(),
NixValue::Apply { .. } | NixValue::Lambda { .. } | NixValue::If { .. }
);
if needs_parens {
f.write_char('(')?;
}
write_value(arg, f, depth)?;
if needs_parens {
f.write_char(')')?;
}
Ok(())
}
NixValue::BinOp { op, lhs, rhs } => {
write_value(lhs, f, depth)?;
f.write_char(' ')?;
f.write_str(op)?;
f.write_char(' ')?;
write_value(rhs, f, depth)
}
NixValue::HasAttr { set, attr } => {
write_value(set, f, depth)?;
f.write_str(" ? ")?;
f.write_str(attr)
}
NixValue::AttrOr { set, attr, fallback } => {
write_value(set, f, depth)?;
f.write_char('.')?;
f.write_str(attr)?;
f.write_str(" or ")?;
write_value(fallback, f, depth)
}
NixValue::Spread => f.write_str("..."),
NixValue::Inherit { scope, names } => {
f.write_str("inherit")?;
if let Some(s) = scope {
f.write_str(" (")?;
write_value(s, f, depth)?;
f.write_char(')')?;
}
for n in names {
f.write_char(' ')?;
f.write_str(n)?;
}
f.write_char(';')
}
NixValue::Interpolated(segs) => {
f.write_char('"')?;
for seg in segs {
match seg {
InterpSegment::Lit(s) => {
for c in s.chars() {
match c {
'"' => f.write_str("\\\"")?,
'\\' => f.write_str("\\\\")?,
'$' => f.write_str("\\$")?,
_ => f.write_char(c)?,
}
}
}
InterpSegment::Expr(e) => {
f.write_str("${")?;
write_value(e, f, depth)?;
f.write_char('}')?;
}
}
}
f.write_char('"')
}
}
}
fn write_attrset(
set: &IndexMap<String, NixValue>,
_rec: bool,
f: &mut Formatter<'_>,
depth: usize,
) -> fmt::Result {
if set.is_empty() {
return f.write_str("{ }");
}
f.write_str("{\n")?;
for (k, v) in set {
indent(f, depth + 1)?;
f.write_str(k)?;
f.write_str(" = ")?;
write_value(v, f, depth + 1)?;
f.write_str(";\n")?;
}
indent(f, depth)?;
f.write_char('}')
}
fn write_lambda_param(p: &LambdaParam, f: &mut Formatter<'_>, depth: usize) -> fmt::Result {
match p {
LambdaParam::Single(name) => f.write_str(name),
LambdaParam::Set { fields, wildcard, bind_all } => {
f.write_str("{ ")?;
for (i, (name, default)) in fields.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_str(name)?;
if let Some(d) = default {
f.write_str(" ? ")?;
write_value(d, f, depth)?;
}
}
if *wildcard {
if !fields.is_empty() {
f.write_str(", ")?;
}
f.write_str("...")?;
}
f.write_str(" }")?;
if let Some(name) = bind_all {
f.write_char('@')?;
f.write_str(name)?;
}
Ok(())
}
}
}
fn write_nix_string(s: &str, f: &mut Formatter<'_>) -> fmt::Result {
f.write_char('"')?;
for c in s.chars() {
match c {
'"' => f.write_str("\\\"")?,
'\\' => f.write_str("\\\\")?,
_ => f.write_char(c)?,
}
}
f.write_char('"')
}
fn indent(f: &mut Formatter<'_>, depth: usize) -> fmt::Result {
for _ in 0..depth {
f.write_str(" ")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn primitives_render_to_canonical_nix() {
assert_eq!(NixValue::Null.to_string(), "null");
assert_eq!(NixValue::Bool(true).to_string(), "true");
assert_eq!(NixValue::Bool(false).to_string(), "false");
assert_eq!(NixValue::Int(42).to_string(), "42");
assert_eq!(NixValue::s("hi").to_string(), "\"hi\"");
assert_eq!(NixValue::ident("foo").to_string(), "foo");
assert_eq!(NixValue::path(&["a", "b", "c"]).to_string(), "a.b.c");
}
#[test]
fn string_quote_and_backslash_escape() {
let v = NixValue::s(r#"hello "world" \ x"#);
assert_eq!(v.to_string(), r#""hello \"world\" \\ x""#);
}
#[test]
fn empty_list_and_attrset_render_compact() {
assert_eq!(NixValue::List(vec![]).to_string(), "[ ]");
assert_eq!(NixValue::AttrSet(IndexMap::new()).to_string(), "{ }");
}
#[test]
fn attrset_indents_per_field_with_trailing_semicolons() {
let v = NixValue::attrset()
.set("a", NixValue::Int(1))
.set("b", NixValue::s("x"))
.build();
let expected = "{\n a = 1;\n b = \"x\";\n}";
assert_eq!(v.to_string(), expected);
}
#[test]
fn nested_attrset_indents_correctly() {
let inner = NixValue::attrset()
.set("foo", NixValue::Int(1))
.build();
let outer = NixValue::attrset().set("inner", inner).build();
let expected = "{\n inner = {\n foo = 1;\n };\n}";
assert_eq!(outer.to_string(), expected);
}
#[test]
fn list_indents_per_item() {
let v = NixValue::List(vec![NixValue::Int(1), NixValue::Int(2), NixValue::Int(3)]);
assert_eq!(v.to_string(), "[\n 1\n 2\n 3\n]");
}
#[test]
fn lambda_single_param_renders_with_colon_body() {
let v = NixValue::Lambda {
param: LambdaParam::Single("pkgs".into()),
body: Box::new(NixValue::ident("pkgs.lib")),
};
assert_eq!(v.to_string(), "pkgs: pkgs.lib");
}
#[test]
fn lambda_set_param_with_defaults_and_wildcard() {
let v = NixValue::Lambda {
param: LambdaParam::Set {
fields: vec![
("a".into(), None),
("b".into(), Some(NixValue::s("default"))),
],
wildcard: true,
bind_all: None,
},
body: Box::new(NixValue::ident("a")),
};
assert_eq!(v.to_string(), "{ a, b ? \"default\", ... }: a");
}
#[test]
fn let_in_renders_bindings_then_body() {
let mut b = IndexMap::new();
b.insert("x".into(), NixValue::Int(1));
b.insert("y".into(), NixValue::Int(2));
let v = NixValue::Let {
bindings: b,
body: Box::new(NixValue::BinOp {
op: "+".into(),
lhs: Box::new(NixValue::ident("x")),
rhs: Box::new(NixValue::ident("y")),
}),
};
let expected = "let\n x = 1;\n y = 2;\nin\nx + y";
assert_eq!(v.to_string(), expected);
}
#[test]
fn if_then_else_inline_render() {
let v = NixValue::If {
cond: Box::new(NixValue::Bool(true)),
then_branch: Box::new(NixValue::Int(1)),
else_branch: Box::new(NixValue::Int(2)),
};
assert_eq!(v.to_string(), "if true then 1 else 2");
}
#[test]
fn apply_wraps_complex_args_in_parens() {
let inner = NixValue::Apply {
func: Box::new(NixValue::ident("g")),
arg: Box::new(NixValue::ident("x")),
};
let outer = NixValue::Apply {
func: Box::new(NixValue::ident("f")),
arg: Box::new(inner),
};
assert_eq!(outer.to_string(), "f (g x)");
}
#[test]
fn has_attr_and_attr_or() {
let s = NixValue::ident("pkgs");
let h = NixValue::HasAttr {
set: Box::new(s.clone()),
attr: "foo".into(),
};
assert_eq!(h.to_string(), "pkgs ? foo");
let o = NixValue::AttrOr {
set: Box::new(s),
attr: "foo".into(),
fallback: Box::new(NixValue::ident("default")),
};
assert_eq!(o.to_string(), "pkgs.foo or default");
}
#[test]
fn inherit_with_and_without_scope() {
let bare = NixValue::Inherit {
scope: None,
names: vec!["a".into(), "b".into()],
};
assert_eq!(bare.to_string(), "inherit a b;");
let scoped = NixValue::Inherit {
scope: Some(Box::new(NixValue::ident("pkgs"))),
names: vec!["lib".into()],
};
assert_eq!(scoped.to_string(), "inherit (pkgs) lib;");
}
#[test]
fn interpolated_string_escapes_lit_and_emits_expr_segment() {
let v = NixValue::Interpolated(vec![
InterpSegment::Lit("hello ".into()),
InterpSegment::Expr(NixValue::ident("name")),
InterpSegment::Lit("!".into()),
]);
assert_eq!(v.to_string(), "\"hello ${name}!\"");
}
#[test]
fn multiline_string_indents_body() {
let v = NixValue::MultilineStr("line one\nline two".into());
let s = v.to_string();
assert!(s.starts_with("''\n line one\n line two\n''"));
}
#[test]
fn binop_attrset_merge_renders_with_double_slash() {
let lhs = NixValue::ident("pkgs.lib");
let rhs = NixValue::attrset().set("x", NixValue::Int(1)).build();
let v = NixValue::BinOp {
op: "//".into(),
lhs: Box::new(lhs),
rhs: Box::new(rhs),
};
assert_eq!(v.to_string(), "pkgs.lib // {\n x = 1;\n}");
}
}