Skip to main content

sim_lib_lang_javascript/
json.rs

1//! ECMAScript JSON policy composed over `sim-codec-json` text and tree mechanics.
2
3use std::collections::HashSet;
4
5use sim_codec_json::JsonTree;
6use sim_kernel::CodecId;
7
8const JSON_CODEC: CodecId = CodecId(0);
9
10/// JavaScript JSON-domain value. Objects retain insertion order until the
11/// ECMAScript property-order projection is applied.
12#[derive(Clone, Debug, PartialEq)]
13pub enum JavascriptJsonValue {
14    /// JSON null.
15    Null,
16    /// Boolean.
17    Bool(bool),
18    /// Number.
19    Number(f64),
20    /// String.
21    String(String),
22    /// Array.
23    Array(Vec<JavascriptJsonValue>),
24    /// Object as insertion-ordered own string properties.
25    Object(Vec<(String, JavascriptJsonValue)>),
26    /// Value omitted by object serialization or rendered as null in arrays.
27    Undefined,
28}
29/// JSON policy error.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum JavascriptJsonError {
32    /// Canonical parser rejected the input.
33    Parse(String),
34    /// A cycle was found during stringify.
35    Cycle,
36    /// Number is not representable by JSON.
37    NonFinite,
38}
39/// Reviver callback, invoked bottom-up with the empty root key last.
40pub type JsonReviver<'a> = dyn FnMut(&str, JavascriptJsonValue) -> Option<JavascriptJsonValue> + 'a;
41/// Replacer callback, invoked top-down with the empty root key first.
42pub type JsonReplacer<'a> =
43    dyn FnMut(&str, JavascriptJsonValue) -> Option<JavascriptJsonValue> + 'a;
44/// JavaScript `toJSON` hook invoked before the replacer.
45pub type JsonToJson<'a> = dyn FnMut(&str, &JavascriptJsonValue) -> Option<JavascriptJsonValue> + 'a;
46
47/// Parse through the canonical JSON parser, then apply ECMAScript reviver order.
48pub fn parse_javascript_json(
49    text: &str,
50    mut reviver: Option<&mut JsonReviver<'_>>,
51) -> Result<JavascriptJsonValue, JavascriptJsonError> {
52    let parsed = sim_codec_json::parse_json(JSON_CODEC, text)
53        .map_err(|error| JavascriptJsonError::Parse(codec_message(error)))?;
54    let value = from_json_tree(parsed)?;
55    Ok(if let Some(callback) = reviver.as_mut() {
56        walk_reviver("", value, *callback).unwrap_or(JavascriptJsonValue::Undefined)
57    } else {
58        value
59    })
60}
61/// Serialize with `toJSON`, replacer, ECMAScript own-property order, and cycle rejection.
62pub fn stringify_javascript_json(
63    value: &JavascriptJsonValue,
64    mut to_json: Option<&mut JsonToJson<'_>>,
65    mut replacer: Option<&mut JsonReplacer<'_>>,
66) -> Result<Option<String>, JavascriptJsonError> {
67    let mut ancestors = HashSet::new();
68    let projected = project(
69        "",
70        value.clone(),
71        &mut to_json,
72        &mut replacer,
73        &mut ancestors,
74        false,
75    )?;
76    match projected {
77        None | Some(JavascriptJsonValue::Undefined) => Ok(None),
78        Some(value) => {
79            let tree = to_json_tree(value, false)?;
80            sim_codec_json::render_json(JSON_CODEC, &tree)
81                .map(Some)
82                .map_err(|error| JavascriptJsonError::Parse(codec_message(error)))
83        }
84    }
85}
86fn from_json_tree(v: JsonTree) -> Result<JavascriptJsonValue, JavascriptJsonError> {
87    match v {
88        JsonTree::Null => Ok(JavascriptJsonValue::Null),
89        JsonTree::Bool(v) => Ok(JavascriptJsonValue::Bool(v)),
90        number @ JsonTree::Number(_) => number
91            .number_as_f64(JSON_CODEC)
92            .map(JavascriptJsonValue::Number)
93            .map_err(|error| JavascriptJsonError::Parse(codec_message(error))),
94        JsonTree::String(v) => Ok(JavascriptJsonValue::String(v)),
95        JsonTree::Array(v) => v
96            .into_iter()
97            .map(from_json_tree)
98            .collect::<Result<_, _>>()
99            .map(JavascriptJsonValue::Array),
100        JsonTree::Object(v) => v
101            .into_iter()
102            .map(|(key, value)| Ok((key, from_json_tree(value)?)))
103            .collect::<Result<_, _>>()
104            .map(JavascriptJsonValue::Object),
105    }
106}
107fn walk_reviver(
108    key: &str,
109    value: JavascriptJsonValue,
110    reviver: &mut JsonReviver<'_>,
111) -> Option<JavascriptJsonValue> {
112    let walked = match value {
113        JavascriptJsonValue::Array(values) => JavascriptJsonValue::Array(
114            values
115                .into_iter()
116                .enumerate()
117                .map(|(i, v)| {
118                    walk_reviver(&i.to_string(), v, reviver)
119                        .unwrap_or(JavascriptJsonValue::Undefined)
120                })
121                .collect(),
122        ),
123        JavascriptJsonValue::Object(entries) => JavascriptJsonValue::Object(
124            entries
125                .into_iter()
126                .filter_map(|(k, v)| walk_reviver(&k, v, reviver).map(|v| (k, v)))
127                .collect(),
128        ),
129        v => v,
130    };
131    reviver(key, walked)
132}
133fn project(
134    key: &str,
135    mut value: JavascriptJsonValue,
136    to_json: &mut Option<&mut JsonToJson<'_>>,
137    replacer: &mut Option<&mut JsonReplacer<'_>>,
138    ancestors: &mut HashSet<usize>,
139    in_array: bool,
140) -> Result<Option<JavascriptJsonValue>, JavascriptJsonError> {
141    if let Some(hook) = to_json.as_mut()
142        && let Some(v) = hook(key, &value)
143    {
144        value = v;
145    }
146    if let Some(callback) = replacer.as_mut() {
147        let Some(v) = callback(key, value) else {
148            return Ok(None);
149        };
150        value = v;
151    }
152    match value {
153        JavascriptJsonValue::Array(values) => {
154            let identity = values.as_ptr() as usize;
155            if !ancestors.insert(identity) {
156                return Err(JavascriptJsonError::Cycle);
157            }
158            let mut out = Vec::with_capacity(values.len());
159            for (i, v) in values.into_iter().enumerate() {
160                out.push(
161                    project(&i.to_string(), v, to_json, replacer, ancestors, true)?
162                        .unwrap_or(JavascriptJsonValue::Null),
163                );
164            }
165            ancestors.remove(&identity);
166            Ok(Some(JavascriptJsonValue::Array(out)))
167        }
168        JavascriptJsonValue::Object(entries) => {
169            let identity = entries.as_ptr() as usize;
170            if !ancestors.insert(identity) {
171                return Err(JavascriptJsonError::Cycle);
172            }
173            let mut out = Vec::new();
174            for (k, v) in ordered_entries(entries) {
175                if let Some(v) = project(&k, v, to_json, replacer, ancestors, false)?
176                    && !matches!(v, JavascriptJsonValue::Undefined)
177                {
178                    out.push((k, v));
179                }
180            }
181            ancestors.remove(&identity);
182            Ok(Some(JavascriptJsonValue::Object(out)))
183        }
184        JavascriptJsonValue::Undefined if in_array => Ok(Some(JavascriptJsonValue::Null)),
185        JavascriptJsonValue::Undefined => Ok(None),
186        v => Ok(Some(v)),
187    }
188}
189fn ordered_entries(
190    entries: Vec<(String, JavascriptJsonValue)>,
191) -> Vec<(String, JavascriptJsonValue)> {
192    let mut indexed = Vec::new();
193    let mut named = Vec::new();
194    for (order, (key, value)) in entries.into_iter().enumerate() {
195        if let Some(index) = array_index(&key) {
196            indexed.push((index, order, key, value));
197        } else {
198            named.push((order, key, value));
199        }
200    }
201    indexed.sort_by_key(|v| v.0);
202    indexed
203        .into_iter()
204        .map(|(_, _, k, v)| (k, v))
205        .chain(named.into_iter().map(|(_, k, v)| (k, v)))
206        .collect()
207}
208fn array_index(key: &str) -> Option<u32> {
209    let value = key.parse::<u32>().ok()?;
210    if value == u32::MAX || value.to_string() != key {
211        return None;
212    }
213    Some(value)
214}
215fn to_json_tree(v: JavascriptJsonValue, in_array: bool) -> Result<JsonTree, JavascriptJsonError> {
216    Ok(match v {
217        JavascriptJsonValue::Null => JsonTree::Null,
218        JavascriptJsonValue::Bool(v) => JsonTree::Bool(v),
219        JavascriptJsonValue::Number(v) => {
220            JsonTree::number_from_f64(JSON_CODEC, v).map_err(|_| JavascriptJsonError::NonFinite)?
221        }
222        JavascriptJsonValue::String(v) => JsonTree::String(v),
223        JavascriptJsonValue::Array(v) => JsonTree::Array(
224            v.into_iter()
225                .map(|v| to_json_tree(v, true))
226                .collect::<Result<_, _>>()?,
227        ),
228        JavascriptJsonValue::Object(v) => {
229            let mut entries: Vec<(String, JsonTree)> = Vec::new();
230            for (k, v) in v {
231                if !matches!(v, JavascriptJsonValue::Undefined) {
232                    let value = to_json_tree(v, false)?;
233                    if let Some((_, existing)) = entries.iter_mut().find(|(key, _)| key == &k) {
234                        *existing = value;
235                    } else {
236                        entries.push((k, value));
237                    }
238                }
239            }
240            JsonTree::Object(entries)
241        }
242        JavascriptJsonValue::Undefined if in_array => JsonTree::Null,
243        JavascriptJsonValue::Undefined => JsonTree::Null,
244    })
245}
246
247fn codec_message(error: sim_kernel::Error) -> String {
248    match error {
249        sim_kernel::Error::CodecError { message, .. } => message,
250        error => error.to_string(),
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    #[test]
258    fn reviver_is_bottom_up() {
259        let mut keys = Vec::new();
260        let mut r = |key: &str, v| {
261            keys.push(key.to_owned());
262            Some(v)
263        };
264        parse_javascript_json(r#"{"a":[1]}"#, Some(&mut r)).unwrap();
265        assert_eq!(keys, vec!["0", "a", ""]);
266    }
267    #[test]
268    fn replacer_to_json_and_property_order_compose() {
269        let value = JavascriptJsonValue::Object(vec![
270            ("b".into(), JavascriptJsonValue::Number(1.)),
271            ("10".into(), JavascriptJsonValue::Number(10.)),
272            ("2".into(), JavascriptJsonValue::Number(2.)),
273        ]);
274        let mut hook = |_: &str, v: &JavascriptJsonValue| Some(v.clone());
275        let mut replace = |k: &str, v| if k == "b" { None } else { Some(v) };
276        assert_eq!(
277            stringify_javascript_json(&value, Some(&mut hook), Some(&mut replace))
278                .unwrap()
279                .unwrap(),
280            r#"{"2":2.0,"10":10.0}"#
281        );
282    }
283    #[test]
284    fn undefined_policy_matches_arrays_and_objects() {
285        let a = JavascriptJsonValue::Array(vec![JavascriptJsonValue::Undefined]);
286        assert_eq!(
287            stringify_javascript_json(&a, None, None).unwrap(),
288            Some("[null]".into())
289        );
290        assert_eq!(
291            stringify_javascript_json(&JavascriptJsonValue::Undefined, None, None).unwrap(),
292            None
293        );
294    }
295
296    #[test]
297    fn reviver_deletion_distinguishes_objects_arrays_and_root() {
298        let mut reviver = |key: &str, value| (key != "drop").then_some(value);
299        assert_eq!(
300            parse_javascript_json(r#"{"drop":1,"keep":[2]}"#, Some(&mut reviver)).unwrap(),
301            JavascriptJsonValue::Object(vec![(
302                "keep".into(),
303                JavascriptJsonValue::Array(vec![JavascriptJsonValue::Number(2.0)]),
304            )])
305        );
306
307        let mut array_reviver = |key: &str, value| (key != "0").then_some(value);
308        assert_eq!(
309            parse_javascript_json("[1]", Some(&mut array_reviver)).unwrap(),
310            JavascriptJsonValue::Array(vec![JavascriptJsonValue::Undefined])
311        );
312
313        let mut root_reviver = |key: &str, value| (!key.is_empty()).then_some(value);
314        assert_eq!(
315            parse_javascript_json("null", Some(&mut root_reviver)).unwrap(),
316            JavascriptJsonValue::Undefined
317        );
318    }
319
320    #[test]
321    fn to_json_precedes_replacer_at_every_level() {
322        use std::cell::RefCell;
323
324        let value = JavascriptJsonValue::Object(vec![(
325            "item".into(),
326            JavascriptJsonValue::String("before".into()),
327        )]);
328        let events = RefCell::new(Vec::new());
329        let mut hook = |key: &str, value: &JavascriptJsonValue| {
330            events.borrow_mut().push(format!("toJSON:{key}"));
331            Some(value.clone())
332        };
333        let mut replacer = |key: &str, value| {
334            events.borrow_mut().push(format!("replacer:{key}"));
335            Some(value)
336        };
337        assert_eq!(
338            stringify_javascript_json(&value, Some(&mut hook), Some(&mut replacer)).unwrap(),
339            Some(r#"{"item":"before"}"#.into())
340        );
341        assert_eq!(
342            events.into_inner(),
343            ["toJSON:", "replacer:", "toJSON:item", "replacer:item"]
344        );
345    }
346
347    #[test]
348    fn replacer_omits_object_members_nulls_array_cells_and_omits_root() {
349        let value = JavascriptJsonValue::Object(vec![
350            ("gone".into(), JavascriptJsonValue::Bool(true)),
351            (
352                "array".into(),
353                JavascriptJsonValue::Array(vec![JavascriptJsonValue::Bool(true)]),
354            ),
355        ]);
356        let mut replacer = |key: &str, value| (key != "gone" && key != "0").then_some(value);
357        assert_eq!(
358            stringify_javascript_json(&value, None, Some(&mut replacer)).unwrap(),
359            Some(r#"{"array":[null]}"#.into())
360        );
361        let mut root_replacer = |key: &str, value| (!key.is_empty()).then_some(value);
362        assert_eq!(
363            stringify_javascript_json(&value, None, Some(&mut root_replacer)).unwrap(),
364            None
365        );
366    }
367
368    #[test]
369    fn number_failures_duplicates_order_diagnostics_and_exact_text_are_stable() {
370        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
371            assert_eq!(
372                stringify_javascript_json(&JavascriptJsonValue::Number(value), None, None),
373                Err(JavascriptJsonError::NonFinite)
374            );
375        }
376        let value = JavascriptJsonValue::Object(vec![
377            ("4294967295".into(), JavascriptJsonValue::Number(1.0)),
378            ("01".into(), JavascriptJsonValue::Number(2.0)),
379            ("2".into(), JavascriptJsonValue::Number(3.0)),
380            ("1".into(), JavascriptJsonValue::Number(4.0)),
381            ("01".into(), JavascriptJsonValue::Number(5.0)),
382        ]);
383        assert_eq!(
384            stringify_javascript_json(&value, None, None).unwrap(),
385            Some(r#"{"1":4.0,"2":3.0,"4294967295":1.0,"01":5.0}"#.into())
386        );
387        assert!(matches!(
388            parse_javascript_json("{]", None),
389            Err(JavascriptJsonError::Parse(message)) if message.contains("line 1 column 2")
390        ));
391    }
392}