Skip to main content

tatara_lisp_script/stdlib/
toml.rs

1//! TOML parse + stringify. Same `Value` shape as JSON/YAML (objects
2//! become alists of 2-lists) so `alist-get` works uniformly.
3//!
4//!   (toml-parse STR)       → nested Value
5//!   (toml-read PATH)       → parse a file
6//!   (toml-stringify VALUE) → TOML text
7
8use std::sync::Arc;
9
10use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
11use toml::Value as TomlValue;
12
13use crate::script_ctx::ScriptCtx;
14use crate::stdlib::env::str_arg;
15
16pub fn install(interp: &mut Interpreter<ScriptCtx>) {
17    interp.register_fn(
18        "toml-parse",
19        Arity::Exact(1),
20        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
21            let s = str_arg(&args[0], "toml-parse", sp)?;
22            let parsed: TomlValue = s.parse().map_err(|e: toml::de::Error| {
23                EvalError::native_fn("toml-parse", e.to_string(), sp)
24            })?;
25            Ok(toml_to_value(&parsed))
26        },
27    );
28
29    interp.register_fn(
30        "toml-read",
31        Arity::Exact(1),
32        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
33            let path = str_arg(&args[0], "toml-read", sp)?;
34            let body = std::fs::read_to_string(&*path)
35                .map_err(|e| EvalError::native_fn("toml-read", format!("{path}: {e}"), sp))?;
36            let parsed: TomlValue = body.parse().map_err(|e: toml::de::Error| {
37                EvalError::native_fn("toml-read", e.to_string(), sp)
38            })?;
39            Ok(toml_to_value(&parsed))
40        },
41    );
42
43    interp.register_fn(
44        "toml-stringify",
45        Arity::Exact(1),
46        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
47            let tv = value_to_toml(&args[0]).ok_or_else(|| {
48                EvalError::native_fn(
49                    "toml-stringify",
50                    "TOML requires a table at the root".to_string(),
51                    sp,
52                )
53            })?;
54            let s = toml::to_string(&tv)
55                .map_err(|e| EvalError::native_fn("toml-stringify", e.to_string(), sp))?;
56            Ok(Value::Str(Arc::from(s)))
57        },
58    );
59}
60
61fn toml_to_value(t: &TomlValue) -> Value {
62    match t {
63        TomlValue::String(s) => Value::Str(Arc::from(s.as_str())),
64        TomlValue::Integer(n) => Value::Int(*n),
65        TomlValue::Float(f) => Value::Float(*f),
66        TomlValue::Boolean(b) => Value::Bool(*b),
67        TomlValue::Datetime(d) => Value::Str(Arc::from(d.to_string())),
68        TomlValue::Array(xs) => Value::list(xs.iter().map(toml_to_value).collect::<Vec<_>>()),
69        TomlValue::Table(m) => Value::list(
70            m.iter()
71                .map(|(k, v)| {
72                    Value::list(vec![Value::Str(Arc::from(k.as_str())), toml_to_value(v)])
73                })
74                .collect::<Vec<_>>(),
75        ),
76    }
77}
78
79fn value_to_toml(v: &Value) -> Option<TomlValue> {
80    match v {
81        Value::Nil => None,
82        Value::Bool(b) => Some(TomlValue::Boolean(*b)),
83        Value::Int(n) => Some(TomlValue::Integer(*n)),
84        Value::Float(f) => Some(TomlValue::Float(*f)),
85        Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
86            Some(TomlValue::String(s.as_ref().to_owned()))
87        }
88        Value::List(xs) => {
89            let looks_like_table = !xs.is_empty()
90                && xs.iter().all(|entry| {
91                    if let Value::List(pair) = entry {
92                        pair.len() == 2
93                            && matches!(
94                                pair[0],
95                                Value::Str(_) | Value::Symbol(_) | Value::Keyword(_)
96                            )
97                    } else {
98                        false
99                    }
100                });
101            if looks_like_table {
102                let mut m = toml::map::Map::new();
103                for entry in xs.iter() {
104                    if let Value::List(pair) = entry {
105                        let k = match &pair[0] {
106                            Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
107                                s.as_ref().to_owned()
108                            }
109                            _ => unreachable!(),
110                        };
111                        if let Some(v) = value_to_toml(&pair[1]) {
112                            m.insert(k, v);
113                        }
114                    }
115                }
116                Some(TomlValue::Table(m))
117            } else {
118                Some(TomlValue::Array(
119                    xs.iter().filter_map(value_to_toml).collect(),
120                ))
121            }
122        }
123        _ => None,
124    }
125}