//! Stable JSON helpers for cache-sensitive request bodies. use serde_json::Value; use sha2::{Digest, Sha256}; pub(crate) fn canonicalize_value(value: Value) -> Value { match value { Value::Array(values) => Value::Array(values.into_iter().map(canonicalize_value).collect()), Value::Object(map) => { let mut entries = map.into_iter().collect::>(); entries.sort_by(|(left, _), (right, _)| left.cmp(right)); let mut sorted = serde_json::Map::new(); for (key, value) in entries { sorted.insert(key, canonicalize_value(value)); } Value::Object(sorted) } other => other, } } pub(crate) fn canonical_json_string(value: &Value) -> String { match value { Value::Null => "null".to_string(), Value::Bool(value) => value.to_string(), Value::Number(value) => value.to_string(), Value::String(value) => serde_json::to_string(value) .expect("serializing a JSON string for canonical output should not fail"), Value::Array(values) => { let parts = values.iter().map(canonical_json_string).collect::>(); format!("[{}]", parts.join(",")) } Value::Object(map) => { let mut entries = map.iter().collect::>(); entries.sort_by_key(|(left, _)| *left); let parts = entries .into_iter() .map(|(key, value)| { let key = serde_json::to_string(key).expect( "serializing a JSON object key for canonical output should not fail", ); format!("{key}:{}", canonical_json_string(value)) }) .collect::>(); format!("{{{}}}", parts.join(",")) } } } pub(crate) fn short_value_hash(value: Option<&Value>) -> String { let Some(value) = value else { return "absent".to_string(); }; short_sha256_hex(canonical_json_string(value).as_bytes()) } pub(crate) fn short_sha256_hex(bytes: &[u8]) -> String { let digest = Sha256::digest(bytes); digest .iter() .take(8) .map(|byte| format!("{byte:02x}")) .collect::() } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn canonical_json_string_sorts_nested_object_keys() { let left = json!({ "b": 2, "a": { "d": true, "c": [3, {"z": 1, "y": 2}] } }); let right = json!({ "a": { "c": [3, {"y": 2, "z": 1}], "d": true }, "b": 2 }); assert_eq!(canonical_json_string(&left), canonical_json_string(&right)); assert_eq!( short_value_hash(Some(&left)), short_value_hash(Some(&right)) ); } #[test] fn canonicalize_value_sorts_map_storage_order() { let value = canonicalize_value(json!({"b": 2, "a": 1})); assert_eq!(serde_json::to_string(&value).unwrap(), r#"{"a":1,"b":2}"#); } }