Skip to main content

tatara_lisp_script/stdlib/
json.rs

1//! JSON parse + stringify, mapping to the tatara-lisp `Value` tree.
2//!
3//!   (json-parse STR)      → nested Value (null → nil, objects → alist)
4//!   (json-stringify V)    → string
5//!   (alist-get ALIST KEY) → value at KEY, or nil
6//!   (alist-get ALIST KEY DEFAULT) → value at KEY, or DEFAULT
7
8use std::sync::Arc;
9
10use serde_json::Value as JsonValue;
11use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
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        "json-parse",
19        Arity::Exact(1),
20        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
21            let s = str_arg(&args[0], "json-parse", sp)?;
22            let parsed: JsonValue = serde_json::from_str(&s)
23                .map_err(|e| EvalError::native_fn("json-parse", e.to_string(), sp))?;
24            Ok(json_to_value(&parsed))
25        },
26    );
27
28    interp.register_fn(
29        "json-stringify",
30        Arity::Exact(1),
31        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
32            let s = serde_json::to_string(&value_to_json(&args[0]))
33                .map_err(|e| EvalError::native_fn("json-stringify", e.to_string(), sp))?;
34            Ok(Value::Str(Arc::from(s)))
35        },
36    );
37
38    interp.register_fn(
39        "alist-get",
40        Arity::Range(2, 3),
41        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
42            let key = match &args[1] {
43                Value::Str(s) => s.clone(),
44                Value::Symbol(s) | Value::Keyword(s) => s.clone(),
45                other => {
46                    return Err(EvalError::native_fn(
47                        "alist-get",
48                        format!(
49                            "key must be string/symbol/keyword, got {}",
50                            other.type_name()
51                        ),
52                        sp,
53                    ))
54                }
55            };
56            let default = args.get(2).cloned().unwrap_or(Value::Nil);
57            Ok(alist_lookup(&args[0], &key).unwrap_or(default))
58        },
59    );
60}
61
62/// Convert a `serde_json::Value` into a tatara-lisp `Value`.
63/// Objects become association lists: `((key . v) (key . v) ...)` where
64/// each pair is a 2-element list for easy alist-get lookup.
65pub fn json_to_value(j: &JsonValue) -> Value {
66    match j {
67        JsonValue::Null => Value::Nil,
68        JsonValue::Bool(b) => Value::Bool(*b),
69        JsonValue::Number(n) => {
70            if let Some(i) = n.as_i64() {
71                Value::Int(i)
72            } else {
73                Value::Float(n.as_f64().unwrap_or(0.0))
74            }
75        }
76        JsonValue::String(s) => Value::Str(Arc::from(s.as_str())),
77        JsonValue::Array(xs) => Value::list(xs.iter().map(json_to_value).collect::<Vec<_>>()),
78        JsonValue::Object(m) => Value::list(
79            m.iter()
80                .map(|(k, v)| {
81                    Value::list(vec![Value::Str(Arc::from(k.as_str())), json_to_value(v)])
82                })
83                .collect::<Vec<_>>(),
84        ),
85    }
86}
87
88/// Convert a tatara-lisp `Value` into a `serde_json::Value` for serialization.
89/// Closures / native fns / foreign / quoted-sexp collapse to `null`.
90pub fn value_to_json(v: &Value) -> JsonValue {
91    match v {
92        Value::Nil => JsonValue::Null,
93        Value::Bool(b) => JsonValue::Bool(*b),
94        Value::Int(n) => JsonValue::Number((*n).into()),
95        Value::Float(n) => serde_json::Number::from_f64(*n)
96            .map(JsonValue::Number)
97            .unwrap_or(JsonValue::Null),
98        Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
99            JsonValue::String(s.as_ref().to_owned())
100        }
101        Value::List(xs) => {
102            // Heuristic: if every element is a 2-list with a string first,
103            // treat it as an object; else array.
104            let looks_like_object = !xs.is_empty()
105                && xs.iter().all(|entry| {
106                    if let Value::List(pair) = entry {
107                        pair.len() == 2
108                            && matches!(
109                                pair[0],
110                                Value::Str(_) | Value::Symbol(_) | Value::Keyword(_)
111                            )
112                    } else {
113                        false
114                    }
115                });
116            if looks_like_object {
117                let mut m = serde_json::Map::with_capacity(xs.len());
118                for entry in xs.iter() {
119                    if let Value::List(pair) = entry {
120                        let k = match &pair[0] {
121                            Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
122                                s.as_ref().to_owned()
123                            }
124                            _ => unreachable!(),
125                        };
126                        m.insert(k, value_to_json(&pair[1]));
127                    }
128                }
129                JsonValue::Object(m)
130            } else {
131                JsonValue::Array(xs.iter().map(value_to_json).collect())
132            }
133        }
134        _ => JsonValue::Null,
135    }
136}
137
138/// Look up `key` in an alist represented as a list of 2-element lists.
139fn alist_lookup(alist: &Value, key: &str) -> Option<Value> {
140    let Value::List(entries) = alist else {
141        return None;
142    };
143    for entry in entries.iter() {
144        let Value::List(pair) = entry else { continue };
145        if pair.len() != 2 {
146            continue;
147        }
148        let matches = match &pair[0] {
149            Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.as_ref() == key,
150            _ => false,
151        };
152        if matches {
153            return Some(pair[1].clone());
154        }
155    }
156    None
157}