//! Canonical JSON serialization. //! //! Writes JSON without whitespace and with lexicographically sorted //! object keys. This makes the representation deterministic across //! runs, platforms, and `serde_json` versions, which is what makes it //! suitable as the pre-image for [`crate::def_hash`] / //! [`crate::module_hash`]. //! //! The single entry point is [`to_bytes`]. This module deliberately //! does **not** parse, validate, or hash — it only emits bytes. It also //! does not attempt to be a general-purpose canonical-JSON library; //! number-formatting follows whatever `serde_json::Number::to_string` //! does, which is fine for the AILang AST (integers and short strings; //! no floats). //! //! # Examples //! //! ```ignore //! use ailang_core::canonical; //! use serde_json::json; //! //! // Object keys are sorted; no whitespace appears in the output. //! let bytes = canonical::to_bytes(&json!({ "b": 1, "a": 2 })); //! assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#); //! ``` use std::io::Write; /// Serialize `value` to canonical JSON bytes. /// /// Object keys are sorted lexicographically, no whitespace is emitted, /// and arrays preserve their input order. The output is the byte /// pre-image used by [`crate::def_hash`] and /// [`crate::workspace::module_hash`]; if you change what bytes this /// function emits for a given value, all on-disk hashes change with it. /// /// # Panics /// /// Panics if `value` cannot be converted to a `serde_json::Value` /// (i.e. its `Serialize` impl errors). For the AST types in /// [`crate::ast`] this never happens. pub fn to_bytes(value: &T) -> Vec { let v = serde_json::to_value(value).expect("serializable"); let mut out = Vec::new(); write_value(&v, &mut out).expect("write to Vec"); out } fn write_value(v: &serde_json::Value, out: &mut Vec) -> std::io::Result<()> { use serde_json::Value; match v { Value::Null => out.write_all(b"null"), Value::Bool(true) => out.write_all(b"true"), Value::Bool(false) => out.write_all(b"false"), Value::Number(n) => out.write_all(n.to_string().as_bytes()), Value::String(s) => { let escaped = serde_json::to_string(s).expect("string serializable"); out.write_all(escaped.as_bytes()) } Value::Array(arr) => { out.write_all(b"[")?; for (i, item) in arr.iter().enumerate() { if i > 0 { out.write_all(b",")?; } write_value(item, out)?; } out.write_all(b"]") } Value::Object(map) => { let mut keys: Vec<&String> = map.keys().collect(); keys.sort(); out.write_all(b"{")?; for (i, k) in keys.iter().enumerate() { if i > 0 { out.write_all(b",")?; } let kj = serde_json::to_string(k).expect("key serializable"); out.write_all(kj.as_bytes())?; out.write_all(b":")?; write_value(&map[*k], out)?; } out.write_all(b"}") } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn sorts_keys() { let v = json!({ "b": 1, "a": 2 }); let bytes = to_bytes(&v); assert_eq!(std::str::from_utf8(&bytes).unwrap(), r#"{"a":2,"b":1}"#); } #[test] fn nested_keys_sorted() { let v = json!({ "z": { "y": 1, "x": [3, { "b": 2, "a": 1 }] } }); let bytes = to_bytes(&v); assert_eq!( std::str::from_utf8(&bytes).unwrap(), r#"{"z":{"x":[3,{"a":1,"b":2}],"y":1}}"# ); } #[test] fn no_whitespace() { let v = json!({ "a": 1, "b": [1, 2, 3] }); let bytes = to_bytes(&v); let s = std::str::from_utf8(&bytes).unwrap(); assert!(!s.contains(' ')); assert!(!s.contains('\n')); } }