dotloom_document/
canonical.rs1use std::collections::BTreeMap;
9
10use serde_json::Value;
11
12use crate::{DocError, Document};
13
14fn normalize(v: Value) -> Value {
15 match v {
16 Value::Object(m) => {
17 let sorted: BTreeMap<String, Value> = m.into_iter().map(|(k, v)| (k, normalize(v))).collect();
18 Value::Object(sorted.into_iter().collect())
19 }
20 Value::Array(a) => Value::Array(a.into_iter().map(normalize).collect()),
21 Value::Number(n) => match n.as_f64() {
22 Some(f) if f == 0.0 && n.is_f64() => serde_json::json!(0.0),
23 _ => Value::Number(n),
24 },
25 other => other,
26 }
27}
28
29#[must_use]
31pub fn fnv1a64(bytes: &[u8]) -> u64 {
32 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
33 for b in bytes {
34 h ^= u64::from(*b);
35 h = h.wrapping_mul(0x0100_0000_01b3);
36 }
37 h
38}
39
40impl Document {
41 pub fn canonical_value(&self) -> Result<Value, DocError> {
43 let v = serde_json::to_value(self).map_err(|e| DocError::Malformed(e.to_string()))?;
44 Ok(normalize(v))
45 }
46
47 pub fn canonical_json(&self) -> Result<String, DocError> {
49 let v = self.canonical_value()?;
50 serde_json::to_string(&v).map_err(|e| DocError::Malformed(e.to_string()))
51 }
52
53 pub fn content_hash(&self) -> Result<String, DocError> {
55 Ok(format!("{:016x}", fnv1a64(self.canonical_json()?.as_bytes())))
56 }
57
58 #[must_use]
60 pub fn semantic_eq(&self, other: &Self) -> bool {
61 match (self.canonical_json(), other.canonical_json()) {
62 (Ok(a), Ok(b)) => a == b,
63 _ => false,
64 }
65 }
66}