use serde_json::{Map, Value as JsonValue};
use sim_codec::DecodeBudget;
use sim_kernel::{CodecId, Expr, NumberLiteral, Result, Symbol};
use crate::expr_json;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JsonProjectionMode {
TaggedCanonical,
UntaggedInterop,
TextLossy,
}
pub fn json_number_to_u64(value: &JsonValue) -> Option<u64> {
value
.as_u64()
.or_else(|| value.as_i64().and_then(|n| u64::try_from(n).ok()))
}
pub fn project_expr_to_json(expr: &Expr, mode: JsonProjectionMode) -> JsonValue {
match mode {
JsonProjectionMode::TaggedCanonical => expr_json::expr_to_json(expr),
JsonProjectionMode::UntaggedInterop => untagged_expr_to_json(expr),
JsonProjectionMode::TextLossy => JsonValue::String(format!("{expr:?}")),
}
}
pub fn project_json_to_expr(value: &JsonValue, mode: JsonProjectionMode) -> Expr {
match mode {
JsonProjectionMode::TaggedCanonical
| JsonProjectionMode::UntaggedInterop
| JsonProjectionMode::TextLossy => untagged_json_to_expr(value),
}
}
pub fn project_json_to_expr_budgeted(
value: &JsonValue,
_mode: JsonProjectionMode,
codec: CodecId,
budget: &mut DecodeBudget,
depth: usize,
) -> Result<Expr> {
budget.enter_node(codec, depth)?;
match value {
JsonValue::Null => Ok(Expr::Nil),
JsonValue::Bool(value) => Ok(Expr::Bool(*value)),
JsonValue::Number(number) => Ok(Expr::Number(NumberLiteral {
domain: Symbol::qualified("numbers", "f64"),
canonical: number.to_string(),
})),
JsonValue::String(value) => {
budget.check_string_bytes(codec, value.len())?;
Ok(Expr::String(value.clone()))
}
JsonValue::Array(items) => {
budget.check_collection_len(codec, items.len())?;
let items = items
.iter()
.map(|item| {
project_json_to_expr_budgeted(
item,
JsonProjectionMode::UntaggedInterop,
codec,
budget,
depth + 1,
)
})
.collect::<Result<Vec<_>>>()?;
Ok(Expr::List(items))
}
JsonValue::Object(entries) => {
budget.check_collection_len(codec, entries.len())?;
let entries = entries
.iter()
.map(|(key, value)| {
Ok((
Expr::Symbol(Symbol::new(key.clone())),
project_json_to_expr_budgeted(
value,
JsonProjectionMode::UntaggedInterop,
codec,
budget,
depth + 1,
)?,
))
})
.collect::<Result<Vec<_>>>()?;
Ok(Expr::Map(entries))
}
}
}
fn untagged_expr_to_json(expr: &Expr) -> JsonValue {
match expr {
Expr::Nil => JsonValue::Null,
Expr::Bool(value) => JsonValue::Bool(*value),
Expr::Number(number) => serde_json::from_str(&number.canonical)
.unwrap_or_else(|_| JsonValue::String(number.canonical.clone())),
Expr::String(value) => JsonValue::String(value.clone()),
Expr::Symbol(symbol) | Expr::Local(symbol) => JsonValue::String(symbol.to_string()),
Expr::List(items) | Expr::Vector(items) => {
JsonValue::Array(items.iter().map(untagged_expr_to_json).collect())
}
Expr::Map(entries) => {
let mut object = Map::new();
for (key, value) in entries {
object.insert(untagged_expr_key(key), untagged_expr_to_json(value));
}
JsonValue::Object(object)
}
other => JsonValue::String(format!("{other:?}")),
}
}
fn untagged_expr_key(expr: &Expr) -> String {
match expr {
Expr::String(value) => value.clone(),
Expr::Symbol(symbol) | Expr::Local(symbol) => symbol.as_qualified_str(),
other => format!("{other:?}"),
}
}
fn untagged_json_to_expr(value: &JsonValue) -> Expr {
match value {
JsonValue::Null => Expr::Nil,
JsonValue::Bool(value) => Expr::Bool(*value),
JsonValue::Number(number) => Expr::Number(NumberLiteral {
domain: Symbol::qualified("numbers", "f64"),
canonical: number.to_string(),
}),
JsonValue::String(value) => Expr::String(value.clone()),
JsonValue::Array(items) => Expr::List(items.iter().map(untagged_json_to_expr).collect()),
JsonValue::Object(entries) => Expr::Map(
entries
.iter()
.map(|(key, value)| {
(
Expr::Symbol(Symbol::new(key.clone())),
untagged_json_to_expr(value),
)
})
.collect(),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn untagged_roundtrips_plain_object() {
let expr = Expr::Map(vec![
(
Expr::Symbol(Symbol::new("name")),
Expr::String("ada".to_owned()),
),
(Expr::Symbol(Symbol::new("flag")), Expr::Bool(true)),
]);
let json = project_expr_to_json(&expr, JsonProjectionMode::UntaggedInterop);
assert_eq!(json["name"], JsonValue::String("ada".to_owned()));
assert_eq!(json["flag"], JsonValue::Bool(true));
}
#[test]
fn text_lossy_is_debug_string() {
let expr = Expr::Bool(true);
let json = project_expr_to_json(&expr, JsonProjectionMode::TextLossy);
assert_eq!(json, JsonValue::String(format!("{expr:?}")));
}
}