use std::sync::Arc;
use serde_json::Value as JsonValue;
use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
use crate::script_ctx::ScriptCtx;
use crate::stdlib::env::str_arg;
pub fn install(interp: &mut Interpreter<ScriptCtx>) {
interp.register_fn(
"json-parse",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = str_arg(&args[0], "json-parse", sp)?;
let parsed: JsonValue = serde_json::from_str(&s)
.map_err(|e| EvalError::native_fn("json-parse", e.to_string(), sp))?;
Ok(json_to_value(&parsed))
},
);
interp.register_fn(
"json-stringify",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = serde_json::to_string(&value_to_json(&args[0]))
.map_err(|e| EvalError::native_fn("json-stringify", e.to_string(), sp))?;
Ok(Value::Str(Arc::from(s)))
},
);
interp.register_fn(
"alist-get",
Arity::Range(2, 3),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let key = match &args[1] {
Value::Str(s) => s.clone(),
Value::Symbol(s) | Value::Keyword(s) => s.clone(),
other => {
return Err(EvalError::native_fn(
"alist-get",
format!(
"key must be string/symbol/keyword, got {}",
other.type_name()
),
sp,
))
}
};
let default = args.get(2).cloned().unwrap_or(Value::Nil);
Ok(alist_lookup(&args[0], &key).unwrap_or(default))
},
);
}
pub fn json_to_value(j: &JsonValue) -> Value {
match j {
JsonValue::Null => Value::Nil,
JsonValue::Bool(b) => Value::Bool(*b),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else {
Value::Float(n.as_f64().unwrap_or(0.0))
}
}
JsonValue::String(s) => Value::Str(Arc::from(s.as_str())),
JsonValue::Array(xs) => Value::list(xs.iter().map(json_to_value).collect::<Vec<_>>()),
JsonValue::Object(m) => Value::list(
m.iter()
.map(|(k, v)| {
Value::list(vec![Value::Str(Arc::from(k.as_str())), json_to_value(v)])
})
.collect::<Vec<_>>(),
),
}
}
pub fn value_to_json(v: &Value) -> JsonValue {
match v {
Value::Nil => JsonValue::Null,
Value::Bool(b) => JsonValue::Bool(*b),
Value::Int(n) => JsonValue::Number((*n).into()),
Value::Float(n) => serde_json::Number::from_f64(*n)
.map(JsonValue::Number)
.unwrap_or(JsonValue::Null),
Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
JsonValue::String(s.as_ref().to_owned())
}
Value::List(xs) => {
let looks_like_object = !xs.is_empty()
&& xs.iter().all(|entry| {
if let Value::List(pair) = entry {
pair.len() == 2
&& matches!(
pair[0],
Value::Str(_) | Value::Symbol(_) | Value::Keyword(_)
)
} else {
false
}
});
if looks_like_object {
let mut m = serde_json::Map::with_capacity(xs.len());
for entry in xs.iter() {
if let Value::List(pair) = entry {
let k = match &pair[0] {
Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => {
s.as_ref().to_owned()
}
_ => unreachable!(),
};
m.insert(k, value_to_json(&pair[1]));
}
}
JsonValue::Object(m)
} else {
JsonValue::Array(xs.iter().map(value_to_json).collect())
}
}
_ => JsonValue::Null,
}
}
fn alist_lookup(alist: &Value, key: &str) -> Option<Value> {
let Value::List(entries) = alist else {
return None;
};
for entry in entries.iter() {
let Value::List(pair) = entry else { continue };
if pair.len() != 2 {
continue;
}
let matches = match &pair[0] {
Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.as_ref() == key,
_ => false,
};
if matches {
return Some(pair[1].clone());
}
}
None
}