use std::collections::BTreeMap;
use crate::{resolve, SemaError, Value, ValueView};
pub fn value_to_json(val: &Value) -> Result<serde_json::Value, SemaError> {
match val.view() {
ValueView::Nil => Ok(serde_json::Value::Null),
ValueView::Bool(b) => Ok(serde_json::Value::Bool(b)),
ValueView::Int(n) => Ok(serde_json::Value::Number(n.into())),
ValueView::Float(f) => serde_json::Number::from_f64(f)
.map(serde_json::Value::Number)
.ok_or_else(|| SemaError::eval("cannot encode NaN/Infinity as JSON")),
ValueView::String(s) => Ok(serde_json::Value::String(s.to_string())),
ValueView::Keyword(s) => Ok(serde_json::Value::String(resolve(s))),
ValueView::Symbol(s) => Ok(serde_json::Value::String(resolve(s))),
ValueView::List(items) | ValueView::Vector(items) => {
let arr: Result<Vec<_>, _> = items.iter().map(value_to_json).collect();
Ok(serde_json::Value::Array(arr?))
}
ValueView::Map(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map.iter() {
obj.insert(key_to_string(k), value_to_json(v)?);
}
Ok(serde_json::Value::Object(obj))
}
ValueView::HashMap(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map.iter() {
obj.insert(key_to_string(k), value_to_json(v)?);
}
Ok(serde_json::Value::Object(obj))
}
_ => Err(SemaError::eval(format!(
"cannot encode {} as JSON",
val.type_name()
))),
}
}
pub fn value_to_json_lossy(val: &Value) -> serde_json::Value {
match val.view() {
ValueView::Nil => serde_json::Value::Null,
ValueView::Bool(b) => serde_json::Value::Bool(b),
ValueView::Int(n) => serde_json::Value::Number(n.into()),
ValueView::Float(f) => serde_json::Number::from_f64(f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
ValueView::String(s) => serde_json::Value::String(s.to_string()),
ValueView::Keyword(s) => serde_json::Value::String(resolve(s)),
ValueView::Symbol(s) => serde_json::Value::String(resolve(s)),
ValueView::List(items) | ValueView::Vector(items) => {
serde_json::Value::Array(items.iter().map(value_to_json_lossy).collect())
}
ValueView::Map(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map.iter() {
obj.insert(key_to_string(k), value_to_json_lossy(v));
}
serde_json::Value::Object(obj)
}
ValueView::HashMap(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map.iter() {
obj.insert(key_to_string(k), value_to_json_lossy(v));
}
serde_json::Value::Object(obj)
}
_ => serde_json::Value::String(val.to_string()),
}
}
pub fn json_to_value(json: &serde_json::Value) -> Value {
match json {
serde_json::Value::Null => Value::nil(),
serde_json::Value::Bool(b) => Value::bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::int(i)
} else if let Some(f) = n.as_f64() {
Value::float(f)
} else {
Value::nil()
}
}
serde_json::Value::String(s) => Value::string(s),
serde_json::Value::Array(arr) => Value::list(arr.iter().map(json_to_value).collect()),
serde_json::Value::Object(obj) => {
let mut map = BTreeMap::new();
for (k, v) in obj {
map.insert(Value::keyword(k), json_to_value(v));
}
Value::map(map)
}
}
}
pub fn key_to_string(k: &Value) -> String {
match k.view() {
ValueView::String(s) => s.to_string(),
ValueView::Keyword(s) => resolve(s),
ValueView::Symbol(s) => resolve(s),
_ => k.to_string(),
}
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
#[test]
fn test_lossy_preserves_map_structure_around_nan() {
let mut map = BTreeMap::new();
map.insert(Value::keyword("a"), Value::int(1));
map.insert(Value::keyword("b"), Value::float(f64::NAN));
let val = Value::map(map);
let json = value_to_json_lossy(&val);
assert!(json.is_object(), "expected JSON object, got: {json}");
let obj = json.as_object().unwrap();
assert_eq!(obj.get("a"), Some(&serde_json::Value::Number(1.into())));
assert_eq!(obj.get("b"), Some(&serde_json::Value::Null));
}
#[test]
fn test_strict_errors_on_nan_in_map() {
let mut map = BTreeMap::new();
map.insert(Value::keyword("ok"), Value::int(42));
map.insert(Value::keyword("bad"), Value::float(f64::NAN));
let val = Value::map(map);
let err = value_to_json(&val).unwrap_err();
assert!(
err.to_string().contains("NaN"),
"expected NaN error, got: {err}"
);
}
#[test]
fn test_strict_errors_on_nan_in_list() {
let val = Value::list(vec![Value::int(1), Value::float(f64::NAN)]);
let err = value_to_json(&val).unwrap_err();
assert!(err.to_string().contains("NaN"));
}
#[test]
fn test_lossy_unsupported_type_becomes_string() {
use crate::NativeFn;
let val = Value::native_fn(NativeFn::simple("test-fn", |_| Ok(Value::nil())));
let json = value_to_json_lossy(&val);
assert!(json.is_string(), "expected string, got: {json}");
}
#[test]
fn test_lossy_preserves_list_structure_around_nan() {
let val = Value::list(vec![Value::int(1), Value::float(f64::NAN), Value::int(3)]);
let json = value_to_json_lossy(&val);
assert!(json.is_array(), "expected JSON array, got: {json}");
let arr = json.as_array().unwrap();
assert_eq!(arr[0], serde_json::Value::Number(1.into()));
assert_eq!(arr[1], serde_json::Value::Null);
assert_eq!(arr[2], serde_json::Value::Number(3.into()));
}
#[test]
fn test_value_to_json_nil() {
let json = value_to_json(&Value::nil()).unwrap();
assert_eq!(json, serde_json::Value::Null);
}
#[test]
fn test_value_to_json_bool() {
assert_eq!(
value_to_json(&Value::bool(true)).unwrap(),
serde_json::Value::Bool(true)
);
assert_eq!(
value_to_json(&Value::bool(false)).unwrap(),
serde_json::Value::Bool(false)
);
}
#[test]
fn test_value_to_json_int() {
let json = value_to_json(&Value::int(42)).unwrap();
assert_eq!(json, serde_json::Value::Number(42.into()));
}
#[test]
fn test_value_to_json_float() {
let json = value_to_json(&Value::float(3.14)).unwrap();
assert_eq!(
json,
serde_json::Value::Number(serde_json::Number::from_f64(3.14).unwrap())
);
}
#[test]
fn test_value_to_json_string() {
let json = value_to_json(&Value::string("hello")).unwrap();
assert_eq!(json, serde_json::Value::String("hello".to_string()));
}
#[test]
fn test_value_to_json_keyword() {
let json = value_to_json(&Value::keyword("foo")).unwrap();
assert_eq!(json, serde_json::Value::String("foo".to_string()));
}
#[test]
fn test_value_to_json_symbol() {
let json = value_to_json(&Value::symbol("foo")).unwrap();
assert_eq!(json, serde_json::Value::String("foo".to_string()));
}
#[test]
fn test_value_to_json_list() {
let val = Value::list(vec![Value::int(1), Value::int(2), Value::int(3)]);
let json = value_to_json(&val).unwrap();
assert_eq!(
json,
serde_json::Value::Array(vec![
serde_json::Value::Number(1.into()),
serde_json::Value::Number(2.into()),
serde_json::Value::Number(3.into()),
])
);
}
#[test]
fn test_value_to_json_map() {
let mut map = BTreeMap::new();
map.insert(Value::keyword("x"), Value::int(10));
map.insert(Value::keyword("y"), Value::int(20));
let val = Value::map(map);
let json = value_to_json(&val).unwrap();
let obj = json.as_object().unwrap();
assert_eq!(obj.get("x"), Some(&serde_json::Value::Number(10.into())));
assert_eq!(obj.get("y"), Some(&serde_json::Value::Number(20.into())));
}
#[test]
fn test_json_to_value_null() {
let val = json_to_value(&serde_json::Value::Null);
assert!(val.is_nil());
}
#[test]
fn test_json_to_value_int() {
let val = json_to_value(&serde_json::json!(42));
assert_eq!(val.as_int(), Some(42));
}
#[test]
fn test_json_to_value_string() {
let val = json_to_value(&serde_json::json!("hello"));
assert_eq!(val.as_str(), Some("hello"));
}
#[test]
fn test_json_to_value_array() {
let val = json_to_value(&serde_json::json!([1, 2, 3]));
let items = val.as_list().unwrap();
assert_eq!(items.len(), 3);
assert_eq!(items[0].as_int(), Some(1));
assert_eq!(items[1].as_int(), Some(2));
assert_eq!(items[2].as_int(), Some(3));
}
#[test]
fn test_json_to_value_object() {
let val = json_to_value(&serde_json::json!({"name": "alice", "age": 30}));
match val.view() {
ValueView::Map(map) => {
assert_eq!(
map.get(&Value::keyword("name")),
Some(&Value::string("alice"))
);
assert_eq!(map.get(&Value::keyword("age")), Some(&Value::int(30)));
}
_ => panic!("expected map"),
}
}
#[test]
fn test_json_roundtrip_simple() {
let original = Value::list(vec![
Value::int(1),
Value::string("two"),
Value::bool(true),
Value::nil(),
]);
let json = value_to_json(&original).unwrap();
let roundtripped = json_to_value(&json);
let items = roundtripped.as_list().unwrap();
assert_eq!(items.len(), 4);
assert_eq!(items[0].as_int(), Some(1));
assert_eq!(items[1].as_str(), Some("two"));
assert_eq!(items[2].as_bool(), Some(true));
assert!(items[3].is_nil());
}
#[test]
fn test_key_to_string_keyword() {
let s = key_to_string(&Value::keyword("foo"));
assert_eq!(s, "foo");
}
#[test]
fn test_key_to_string_string() {
let s = key_to_string(&Value::string("bar"));
assert_eq!(s, "bar");
}
#[test]
fn test_key_to_string_int() {
let s = key_to_string(&Value::int(42));
assert_eq!(s, "42");
}
}