Skip to main content

supercode_interchange/orchestration/codec/
canonical.rs

1//! The canonical JSON every snapshot and "unchanged" decision compares:
2//! keys sorted at every depth, two-space pretty print, trailing newline —
3//! `ir.mjs::canonicalJson`. Sorting is explicit because `serde_json`'s own
4//! object order depends on a cargo feature another crate may enable.
5
6use serde_json::{Map, Value};
7
8/// Every object's keys sorted, at every depth.
9pub fn sort_keys(value: &Value) -> Value {
10    match value {
11        Value::Array(items) => Value::Array(items.iter().map(sort_keys).collect()),
12        Value::Object(object) => {
13            let mut keys: Vec<&String> = object.keys().collect();
14            keys.sort();
15            let mut out = Map::new();
16            for key in keys {
17                out.insert(key.clone(), sort_keys(&object[key]));
18            }
19            Value::Object(out)
20        }
21        other => other.clone(),
22    }
23}
24
25/// Stable, secret-free JSON of a value.
26pub fn canonical_json(value: &Value) -> String {
27    let sorted = sort_keys(value);
28    let mut text = pretty(&sorted, 0);
29    text.push('\n');
30    text
31}
32
33/// Two-space pretty print in sorted-key order (independent of any feature).
34fn pretty(value: &Value, depth: usize) -> String {
35    let pad = |d: usize| "  ".repeat(d);
36    match value {
37        Value::Array(items) if items.is_empty() => "[]".into(),
38        Value::Array(items) => {
39            let inner: Vec<String> = items
40                .iter()
41                .map(|v| format!("{}{}", pad(depth + 1), pretty(v, depth + 1)))
42                .collect();
43            format!("[\n{}\n{}]", inner.join(",\n"), pad(depth))
44        }
45        Value::Object(object) if object.is_empty() => "{}".into(),
46        Value::Object(object) => {
47            let mut keys: Vec<&String> = object.keys().collect();
48            keys.sort();
49            let inner: Vec<String> = keys
50                .into_iter()
51                .map(|k| {
52                    format!(
53                        "{}{}: {}",
54                        pad(depth + 1),
55                        serde_json::to_string(k).unwrap(),
56                        pretty(&object[k], depth + 1)
57                    )
58                })
59                .collect();
60            format!("{{\n{}\n{}}}", inner.join(",\n"), pad(depth))
61        }
62        Value::Number(n) => json_number(n),
63        other => serde_json::to_string(other).unwrap(),
64    }
65}
66
67/// JavaScript's number rendering for the numbers a store carries: an integral
68/// float prints without a fraction (`120`, not `120.0`), as `JSON.stringify` does.
69fn json_number(n: &serde_json::Number) -> String {
70    if let Some(f) = n.as_f64() {
71        if n.is_f64() && f.fract() == 0.0 && f.abs() < 1e21 {
72            return format!("{}", f as i64);
73        }
74    }
75    n.to_string()
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn sorted_pretty_with_js_numbers() {
84        let v = serde_json::json!({"b": [1, 2.0, 2.5], "a": {"z": null, "y": "s"}});
85        assert_eq!(canonical_json(&v), "{\n  \"a\": {\n    \"y\": \"s\",\n    \"z\": null\n  },\n  \"b\": [\n    1,\n    2,\n    2.5\n  ]\n}\n");
86        assert_eq!(canonical_json(&serde_json::json!({})), "{}\n");
87    }
88}