1use std::sync::Arc;
34
35use serde_json::Value as JsonValue;
36use tatara_lisp_eval::ffi::Arity;
37use tatara_lisp_eval::{EvalError, Interpreter, MapKey, Value};
38
39pub fn install_json_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
41 interp.register_fn(
42 "json_parse",
43 Arity::Exact(1),
44 |args: &[Value], _h: &mut H, span| {
45 let s = as_str(&args[0], "json_parse", span)?;
46 let parsed: JsonValue = serde_json::from_str(&s)
47 .map_err(|e| EvalError::native_fn("json_parse", e.to_string(), span))?;
48 Ok(json_to_value(&parsed))
49 },
50 );
51
52 interp.register_fn(
53 "json_stringify",
54 Arity::Exact(1),
55 |args: &[Value], _h: &mut H, span| {
56 let s = serde_json::to_string(&value_to_json(&args[0]))
57 .map_err(|e| EvalError::native_fn("json_stringify", e.to_string(), span))?;
58 Ok(Value::Str(Arc::from(s)))
59 },
60 );
61
62 interp.register_fn(
63 "json_get",
64 Arity::Exact(2),
65 |args: &[Value], _h: &mut H, span| {
66 let doc = &args[0];
67 let key = as_str(&args[1], "json_get", span)?;
68 match json_lookup(doc, &key) {
69 Some(v) => Ok(v),
70 None if is_doc(doc) => Ok(Value::Nil),
71 None => Err(EvalError::native_fn(
72 "json_get",
73 format!("cannot read a field from a {}", doc.type_name()),
74 span,
75 )),
76 }
77 },
78 );
79
80 interp.register_fn(
81 "json_get_or",
82 Arity::Exact(3),
83 |args: &[Value], _h: &mut H, span| {
84 let doc = &args[0];
85 let key = as_str(&args[1], "json_get_or", span)?;
86 match json_lookup(doc, &key) {
87 Some(v) => Ok(v),
88 None if is_doc(doc) => Ok(args[2].clone()),
89 None => Err(EvalError::native_fn(
90 "json_get_or",
91 format!("cannot read a field from a {}", doc.type_name()),
92 span,
93 )),
94 }
95 },
96 );
97}
98
99fn is_doc(v: &Value) -> bool {
103 matches!(v, Value::List(_) | Value::Map(_))
104}
105
106fn as_str(v: &Value, fname: &'static str, span: tatara_lisp::Span) -> Result<String, EvalError> {
107 match v {
108 Value::Str(s) => Ok(s.to_string()),
109 Value::Symbol(s) | Value::Keyword(s) => Ok(s.to_string()),
112 other => {
113 Err(EvalError::type_mismatch("a string", other.type_name(), span).into_native(fname))
114 }
115 }
116}
117
118trait NameError {
121 fn into_native(self, fname: &'static str) -> EvalError;
122}
123
124impl NameError for EvalError {
125 fn into_native(self, fname: &'static str) -> EvalError {
126 match self {
127 EvalError::TypeMismatch { expected, got, at } => {
128 EvalError::native_fn(fname, format!("expected {expected}, got {got}"), at)
129 }
130 other => other,
131 }
132 }
133}
134
135pub fn json_to_value(j: &JsonValue) -> Value {
139 match j {
140 JsonValue::Null => Value::Nil,
141 JsonValue::Bool(b) => Value::Bool(*b),
142 JsonValue::Number(n) => {
143 if let Some(i) = n.as_i64() {
144 Value::Int(i)
145 } else {
146 Value::Float(n.as_f64().unwrap_or(0.0))
147 }
148 }
149 JsonValue::String(s) => Value::Str(Arc::from(s.as_str())),
150 JsonValue::Array(xs) => Value::list(xs.iter().map(json_to_value)),
151 JsonValue::Object(m) if m.is_empty() => Value::map([]),
152 JsonValue::Object(m) => Value::list(
153 m.iter()
154 .map(|(k, v)| Value::list([Value::Str(Arc::from(k.as_str())), json_to_value(v)])),
155 ),
156 }
157}
158
159pub fn value_to_json(v: &Value) -> JsonValue {
162 match v {
163 Value::Nil => JsonValue::Null,
164 Value::Bool(b) => JsonValue::Bool(*b),
165 Value::Int(n) => JsonValue::Number((*n).into()),
166 Value::Float(n) => serde_json::Number::from_f64(*n)
167 .map(JsonValue::Number)
168 .unwrap_or(JsonValue::Null),
169 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
170 JsonValue::String(s.as_ref().to_owned())
171 }
172 Value::List(xs) => {
177 let looks_like_object = !xs.is_empty()
178 && xs.iter().all(|entry| {
179 if let Value::List(pair) = entry {
180 pair.len() == 2
181 && matches!(
182 pair[0],
183 Value::Str(_) | Value::Symbol(_) | Value::Keyword(_)
184 )
185 } else {
186 false
187 }
188 });
189 if looks_like_object {
190 let mut m = serde_json::Map::with_capacity(xs.len());
191 for entry in xs.iter() {
192 if let Value::List(pair) = entry {
193 let k = match &pair[0] {
194 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
195 s.as_ref().to_owned()
196 }
197 _ => unreachable!(),
198 };
199 m.insert(k, value_to_json(&pair[1]));
200 }
201 }
202 JsonValue::Object(m)
203 } else {
204 JsonValue::Array(xs.iter().map(value_to_json).collect())
205 }
206 }
207 Value::Map(m) => JsonValue::Object(
212 m.iter()
213 .map(|(k, v)| (map_key_to_json_key(k), value_to_json(v)))
214 .collect(),
215 ),
216 _ => JsonValue::Null,
217 }
218}
219
220fn map_key_to_json_key(k: &MapKey) -> String {
221 match k {
222 MapKey::Str(s) | MapKey::Symbol(s) | MapKey::Keyword(s) => s.as_ref().to_owned(),
223 MapKey::Nil => "null".to_owned(),
224 MapKey::Bool(b) => b.to_string(),
225 MapKey::Int(n) => n.to_string(),
226 MapKey::Float(bits) => f64::from_bits(*bits).to_string(),
227 }
228}
229
230fn json_lookup(doc: &Value, key: &str) -> Option<Value> {
233 match doc {
234 Value::List(entries) => entries.iter().find_map(|entry| {
235 let Value::List(pair) = entry else {
236 return None;
237 };
238 if pair.len() != 2 {
239 return None;
240 }
241 let matches = match &pair[0] {
242 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.as_ref() == key,
243 _ => false,
244 };
245 matches.then(|| pair[1].clone())
246 }),
247 Value::Map(m) => {
248 let k = MapKey::Str(Arc::from(key));
249 m.get(&k).cloned()
250 }
251 _ => None,
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 fn eval(src: &str) -> Value {
260 crate::run(src)
261 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
262 .value
263 }
264
265 fn s(src: &str) -> String {
266 match eval(src) {
267 Value::Str(v) => v.to_string(),
268 other => panic!("{src:?} produced {other:?}"),
269 }
270 }
271
272 fn assert_round_trips(src: &str) {
276 let parsed: JsonValue = serde_json::from_str(src).expect("fixture is valid JSON");
277 let out = value_to_json(&json_to_value(&parsed));
278 assert_eq!(out, parsed, "round-trip changed the document\nin: {src}");
279 }
280
281 #[test]
282 fn parse_and_get_an_object_field() {
283 let v = eval(r#"json_get(json_parse("{\"outcome\":\"ok\"}"), "outcome")"#);
284 assert!(matches!(v, Value::Str(ref x) if &**x == "ok"), "got {v:?}");
285 }
286
287 #[test]
288 fn a_missing_field_is_nil_not_an_error() {
289 assert!(matches!(
290 eval(r#"json_get(json_parse("{\"a\":1}"), "nope")"#),
291 Value::Nil
292 ));
293 }
294
295 #[test]
296 fn json_get_or_returns_the_default() {
297 assert!(matches!(
298 eval(r#"json_get_or(json_parse("{}"), "nope", 42)"#),
299 Value::Int(42)
300 ));
301 }
302
303 #[test]
304 fn nested_objects_read_by_walking_alists() {
305 let src = r#"json_get(
306 json_get(json_parse("{\"outer\":{\"inner\":7}}"), "outer"),
307 "inner")"#;
308 assert!(matches!(eval(src), Value::Int(7)));
309 }
310
311 #[test]
312 fn numbers_and_bools_keep_their_kinds() {
313 assert!(matches!(
314 eval(r#"json_get(json_parse("{\"n\":1,\"b\":true}"), "n")"#),
315 Value::Int(1)
316 ));
317 assert!(matches!(
318 eval(r#"json_get(json_parse("{\"b\":true}"), "b")"#),
319 Value::Bool(true)
320 ));
321 }
322
323 #[test]
324 fn null_is_nil() {
325 assert!(matches!(
326 eval(r#"json_get(json_parse("{\"x\":null}"), "x")"#),
327 Value::Nil
328 ));
329 }
330
331 #[test]
332 fn stringify_reaches_the_parser_shape_back() {
333 assert_eq!(
334 s(r#"json_stringify(json_parse("{\"a\":1,\"b\":[1,2]}"))"#),
335 r#"{"a":1,"b":[1,2]}"#
336 );
337 }
338
339 #[test]
340 fn empty_object_round_trips() {
341 assert_round_trips("{}");
342 assert_round_trips(r#"{"a":{}}"#);
343 assert_round_trips(r#"{"a":{"b":{}}}"#);
344 assert_round_trips(r#"[{},{}]"#);
345 }
346
347 #[test]
348 fn empty_array_is_not_confused_for_an_object() {
349 assert_round_trips("[]");
350 assert_round_trips(r#"{"a":[]}"#);
351 assert_round_trips(r#"{"obj":{},"arr":[]}"#);
352 }
353
354 #[test]
355 fn non_empty_objects_parse_to_alists() {
356 let v = eval(r#"json_parse("{\"a\":1}")"#);
357 assert!(
358 matches!(v, Value::List(_)),
359 "non-empty object must be an alist"
360 );
361 }
362
363 #[test]
364 fn a_parsed_empty_object_is_a_map_and_reads_as_nil() {
365 let v = eval(r#"json_parse("{}")"#);
366 assert!(
367 matches!(v, Value::Map(_)),
368 "an empty object must be a Map so it round-trips"
369 );
370 assert!(
371 matches!(
372 eval(r#"json_get(json_parse("{}"), "anything")"#),
373 Value::Nil
374 ),
375 "an empty object has no fields to read"
376 );
377 }
378
379 #[test]
380 fn unparseable_json_is_a_named_error() {
381 let err = crate::run(r#"json_parse("{not json")"#).expect_err("must raise");
382 assert!(
383 err.to_string().contains("json_parse"),
384 "the error must name the primitive: {err}"
385 );
386 }
387
388 #[test]
389 fn a_wrong_typed_argument_is_a_type_error() {
390 assert!(crate::run(r#"json_parse(42)"#).is_err());
391 assert!(crate::run(r#"json_get("not a doc", "k")"#).is_err());
392 }
393}