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