sim_codec_json/
projection.rs1use serde_json::{Map, Value as JsonValue};
11use sim_codec::DecodeBudget;
12use sim_kernel::{CodecId, Expr, NumberLiteral, Result, Symbol};
13
14use crate::expr_json;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum JsonProjectionMode {
19 TaggedCanonical,
23 UntaggedInterop,
32 TextLossy,
37}
38
39pub fn json_number_to_u64(value: &JsonValue) -> Option<u64> {
44 value
45 .as_u64()
46 .or_else(|| value.as_i64().and_then(|n| u64::try_from(n).ok()))
47}
48
49pub fn project_expr_to_json(expr: &Expr, mode: JsonProjectionMode) -> JsonValue {
51 match mode {
52 JsonProjectionMode::TaggedCanonical => expr_json::expr_to_json(expr),
53 JsonProjectionMode::UntaggedInterop => untagged_expr_to_json(expr),
54 JsonProjectionMode::TextLossy => JsonValue::String(format!("{expr:?}")),
55 }
56}
57
58pub fn project_json_to_expr(value: &JsonValue, mode: JsonProjectionMode) -> Expr {
70 match mode {
71 JsonProjectionMode::TaggedCanonical
74 | JsonProjectionMode::UntaggedInterop
75 | JsonProjectionMode::TextLossy => untagged_json_to_expr(value),
76 }
77}
78
79pub fn project_json_to_expr_budgeted(
87 value: &JsonValue,
88 _mode: JsonProjectionMode,
89 codec: CodecId,
90 budget: &mut DecodeBudget,
91 depth: usize,
92) -> Result<Expr> {
93 budget.enter_node(codec, depth)?;
94 match value {
95 JsonValue::Null => Ok(Expr::Nil),
96 JsonValue::Bool(value) => Ok(Expr::Bool(*value)),
97 JsonValue::Number(number) => Ok(Expr::Number(NumberLiteral {
98 domain: Symbol::qualified("numbers", "f64"),
99 canonical: number.to_string(),
100 })),
101 JsonValue::String(value) => {
102 budget.check_string_bytes(codec, value.len())?;
103 Ok(Expr::String(value.clone()))
104 }
105 JsonValue::Array(items) => {
106 budget.check_collection_len(codec, items.len())?;
107 let items = items
108 .iter()
109 .map(|item| {
110 project_json_to_expr_budgeted(
111 item,
112 JsonProjectionMode::UntaggedInterop,
113 codec,
114 budget,
115 depth + 1,
116 )
117 })
118 .collect::<Result<Vec<_>>>()?;
119 Ok(Expr::List(items))
120 }
121 JsonValue::Object(entries) => {
122 budget.check_collection_len(codec, entries.len())?;
123 let entries = entries
124 .iter()
125 .map(|(key, value)| {
126 Ok((
127 Expr::Symbol(Symbol::new(key.clone())),
128 project_json_to_expr_budgeted(
129 value,
130 JsonProjectionMode::UntaggedInterop,
131 codec,
132 budget,
133 depth + 1,
134 )?,
135 ))
136 })
137 .collect::<Result<Vec<_>>>()?;
138 Ok(Expr::Map(entries))
139 }
140 }
141}
142
143fn untagged_expr_to_json(expr: &Expr) -> JsonValue {
148 match expr {
149 Expr::Nil => JsonValue::Null,
150 Expr::Bool(value) => JsonValue::Bool(*value),
151 Expr::Number(number) => serde_json::from_str(&number.canonical)
152 .unwrap_or_else(|_| JsonValue::String(number.canonical.clone())),
153 Expr::String(value) => JsonValue::String(value.clone()),
154 Expr::Symbol(symbol) | Expr::Local(symbol) => JsonValue::String(symbol.to_string()),
155 Expr::List(items) | Expr::Vector(items) => {
156 JsonValue::Array(items.iter().map(untagged_expr_to_json).collect())
157 }
158 Expr::Map(entries) => {
159 let mut object = Map::new();
160 for (key, value) in entries {
161 object.insert(untagged_expr_key(key), untagged_expr_to_json(value));
162 }
163 JsonValue::Object(object)
164 }
165 other => JsonValue::String(format!("{other:?}")),
166 }
167}
168
169fn untagged_expr_key(expr: &Expr) -> String {
170 match expr {
171 Expr::String(value) => value.clone(),
172 Expr::Symbol(symbol) | Expr::Local(symbol) => symbol.as_qualified_str(),
173 other => format!("{other:?}"),
174 }
175}
176
177fn untagged_json_to_expr(value: &JsonValue) -> Expr {
178 match value {
179 JsonValue::Null => Expr::Nil,
180 JsonValue::Bool(value) => Expr::Bool(*value),
181 JsonValue::Number(number) => Expr::Number(NumberLiteral {
182 domain: Symbol::qualified("numbers", "f64"),
183 canonical: number.to_string(),
184 }),
185 JsonValue::String(value) => Expr::String(value.clone()),
186 JsonValue::Array(items) => Expr::List(items.iter().map(untagged_json_to_expr).collect()),
187 JsonValue::Object(entries) => Expr::Map(
188 entries
189 .iter()
190 .map(|(key, value)| {
191 (
192 Expr::Symbol(Symbol::new(key.clone())),
193 untagged_json_to_expr(value),
194 )
195 })
196 .collect(),
197 ),
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn untagged_roundtrips_plain_object() {
207 let expr = Expr::Map(vec![
208 (
209 Expr::Symbol(Symbol::new("name")),
210 Expr::String("ada".to_owned()),
211 ),
212 (Expr::Symbol(Symbol::new("flag")), Expr::Bool(true)),
213 ]);
214 let json = project_expr_to_json(&expr, JsonProjectionMode::UntaggedInterop);
215 assert_eq!(json["name"], JsonValue::String("ada".to_owned()));
216 assert_eq!(json["flag"], JsonValue::Bool(true));
217 }
218
219 #[test]
220 fn text_lossy_is_debug_string() {
221 let expr = Expr::Bool(true);
222 let json = project_expr_to_json(&expr, JsonProjectionMode::TextLossy);
223 assert_eq!(json, JsonValue::String(format!("{expr:?}")));
224 }
225}