1use std::fmt;
11
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum HashError {
22 #[error("value could not be serialized to canonical JSON")]
24 Serialization(#[source] serde_json::Error),
25}
26
27#[derive(
29 Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
30)]
31#[serde(transparent)]
32pub struct Digest(pub String);
33
34impl Digest {
35 #[must_use]
37 pub fn as_str(&self) -> &str {
38 &self.0
39 }
40
41 #[must_use]
43 pub fn of_bytes(bytes: &[u8]) -> Self {
44 Self(digest_hex(bytes))
45 }
46
47 pub fn of_canonical<T: Serialize + ?Sized>(value: &T) -> Result<Self, HashError> {
49 canonical_digest(value)
50 }
51}
52
53impl fmt::Display for Digest {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.write_str(&self.0)
56 }
57}
58
59impl From<Digest> for String {
60 fn from(value: Digest) -> Self {
61 value.0
62 }
63}
64
65pub fn canonical_json<T: Serialize + ?Sized>(value: &T) -> Result<String, HashError> {
67 let mut json = serde_json::to_value(value).map_err(HashError::Serialization)?;
68 json.sort_all_objects();
69 serde_json::to_string(&json).map_err(HashError::Serialization)
70}
71
72pub fn canonical_value<T: Serialize + ?Sized>(value: &T) -> Result<serde_json::Value, HashError> {
74 let mut json = serde_json::to_value(value).map_err(HashError::Serialization)?;
75 json.sort_all_objects();
76 Ok(json)
77}
78
79#[must_use]
81pub fn digest_hex(bytes: &[u8]) -> String {
82 blake3::hash(bytes).to_hex().to_string()
83}
84
85pub fn canonical_digest<T: Serialize + ?Sized>(value: &T) -> Result<Digest, HashError> {
87 let json = canonical_json(value)?;
88 Ok(Digest(digest_hex(json.as_bytes())))
89}
90
91#[must_use]
101pub fn derive_uuid(domain: &str, parts: &[&str]) -> Uuid {
102 let mut hasher = blake3::Hasher::new();
103 hasher.update(domain.as_bytes());
104 for part in parts {
105 hasher.update(b"\0");
106 hasher.update(part.as_bytes());
107 }
108 let mut bytes = [0_u8; 16];
109 bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
110 uuid::Builder::from_custom_bytes(bytes).into_uuid()
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use serde_json::json;
117
118 #[test]
119 fn canonical_json_sorts_keys_recursively() {
120 let value = json!({"b": {"z": 1, "a": [{"y": 1, "x": 2}]}, "a": 1});
121 assert_eq!(
122 canonical_json(&value).unwrap(),
123 r#"{"a":1,"b":{"a":[{"x":2,"y":1}],"z":1}}"#
124 );
125 }
126
127 #[test]
128 fn derive_uuid_is_stable_domain_separated_and_injective() {
129 let a = derive_uuid("turnframe.test.v1", &["x", "y"]);
130 assert_eq!(a, derive_uuid("turnframe.test.v1", &["x", "y"]));
131 assert_ne!(a, derive_uuid("turnframe.other.v1", &["x", "y"]));
132 assert_ne!(a, derive_uuid("turnframe.test.v1", &["xy"]));
133 assert_ne!(a, derive_uuid("turnframe.test.v1", &["x", "y", "z"]));
134 assert_eq!(a.get_version_num(), 8, "custom, not a time-ordered v7");
135 }
136
137 #[test]
138 fn digest_is_stable_and_order_independent() {
139 let a = canonical_digest(&json!({"x": 1, "y": 2})).unwrap();
140 let b = canonical_digest(&json!({"y": 2, "x": 1})).unwrap();
141 let c = canonical_digest(&json!({"y": 2, "x": 3})).unwrap();
142 assert_eq!(a, b);
143 assert_ne!(a, c);
144 assert_eq!(a.as_str().len(), 64);
145 }
146}