supercode-interchange 0.4.14

Canonical, provider-neutral session interchange primitives for Supercode
Documentation
//! The canonical JSON every snapshot and "unchanged" decision compares:
//! keys sorted at every depth, two-space pretty print, trailing newline —
//! `ir.mjs::canonicalJson`. Sorting is explicit because `serde_json`'s own
//! object order depends on a cargo feature another crate may enable.

use serde_json::{Map, Value};

/// Every object's keys sorted, at every depth.
pub fn sort_keys(value: &Value) -> Value {
    match value {
        Value::Array(items) => Value::Array(items.iter().map(sort_keys).collect()),
        Value::Object(object) => {
            let mut keys: Vec<&String> = object.keys().collect();
            keys.sort();
            let mut out = Map::new();
            for key in keys {
                out.insert(key.clone(), sort_keys(&object[key]));
            }
            Value::Object(out)
        }
        other => other.clone(),
    }
}

/// Stable, secret-free JSON of a value.
pub fn canonical_json(value: &Value) -> String {
    let sorted = sort_keys(value);
    let mut text = pretty(&sorted, 0);
    text.push('\n');
    text
}

/// Two-space pretty print in sorted-key order (independent of any feature).
fn pretty(value: &Value, depth: usize) -> String {
    let pad = |d: usize| "  ".repeat(d);
    match value {
        Value::Array(items) if items.is_empty() => "[]".into(),
        Value::Array(items) => {
            let inner: Vec<String> = items
                .iter()
                .map(|v| format!("{}{}", pad(depth + 1), pretty(v, depth + 1)))
                .collect();
            format!("[\n{}\n{}]", inner.join(",\n"), pad(depth))
        }
        Value::Object(object) if object.is_empty() => "{}".into(),
        Value::Object(object) => {
            let mut keys: Vec<&String> = object.keys().collect();
            keys.sort();
            let inner: Vec<String> = keys
                .into_iter()
                .map(|k| {
                    format!(
                        "{}{}: {}",
                        pad(depth + 1),
                        serde_json::to_string(k).unwrap(),
                        pretty(&object[k], depth + 1)
                    )
                })
                .collect();
            format!("{{\n{}\n{}}}", inner.join(",\n"), pad(depth))
        }
        Value::Number(n) => json_number(n),
        other => serde_json::to_string(other).unwrap(),
    }
}

/// JavaScript's number rendering for the numbers a store carries: an integral
/// float prints without a fraction (`120`, not `120.0`), as `JSON.stringify` does.
fn json_number(n: &serde_json::Number) -> String {
    if let Some(f) = n.as_f64() {
        if n.is_f64() && f.fract() == 0.0 && f.abs() < 1e21 {
            return format!("{}", f as i64);
        }
    }
    n.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sorted_pretty_with_js_numbers() {
        let v = serde_json::json!({"b": [1, 2.0, 2.5], "a": {"z": null, "y": "s"}});
        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");
        assert_eq!(canonical_json(&serde_json::json!({})), "{}\n");
    }
}