use sha2::{Digest, Sha256};
pub fn node_id(hash_input: &serde_json::Value) -> String {
let canonical = canonical_json_bytes(hash_input);
let digest = Sha256::digest(&canonical);
hex::encode(digest)
}
pub fn canonical_json_bytes(value: &serde_json::Value) -> Vec<u8> {
fn sort_keys(v: &serde_json::Value) -> serde_json::Value {
match v {
serde_json::Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
let sorted = keys
.into_iter()
.map(|k| (k.clone(), sort_keys(&map[k])))
.collect();
serde_json::Value::Object(sorted)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(sort_keys).collect())
}
other => other.clone(),
}
}
let sorted = sort_keys(value);
serde_json::to_vec(&sorted).expect("valid JSON value cannot fail to serialize")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn key_order_is_stable() {
let a = json!({ "z": 1, "a": 2, "m": 3 });
let b = json!({ "m": 3, "z": 1, "a": 2 });
assert_eq!(canonical_json_bytes(&a), canonical_json_bytes(&b));
}
#[test]
fn nested_objects_sorted() {
let v = json!({ "b": { "z": 1, "a": 2 }, "a": 1 });
let bytes = canonical_json_bytes(&v);
let s = std::str::from_utf8(&bytes).unwrap();
assert!(s.find('"').is_some());
let parsed: serde_json::Value = serde_json::from_str(s).unwrap();
let obj = parsed.as_object().unwrap();
let keys: Vec<&String> = obj.keys().collect();
assert_eq!(keys[0], "a");
assert_eq!(keys[1], "b");
}
#[test]
fn node_id_is_hex_sha256() {
let id = node_id(&json!({ "type": "intent", "text": "hello" }));
assert_eq!(id.len(), 64);
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn same_content_same_id() {
let v = json!({ "type": "intent", "text": "add JWT verification" });
assert_eq!(node_id(&v), node_id(&v));
}
}