use std::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HashError {
#[error("value could not be serialized to canonical JSON")]
Serialization(#[source] serde_json::Error),
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(transparent)]
pub struct Digest(pub String);
impl Digest {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn of_bytes(bytes: &[u8]) -> Self {
Self(digest_hex(bytes))
}
pub fn of_canonical<T: Serialize + ?Sized>(value: &T) -> Result<Self, HashError> {
canonical_digest(value)
}
}
impl fmt::Display for Digest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<Digest> for String {
fn from(value: Digest) -> Self {
value.0
}
}
pub fn canonical_json<T: Serialize + ?Sized>(value: &T) -> Result<String, HashError> {
let mut json = serde_json::to_value(value).map_err(HashError::Serialization)?;
json.sort_all_objects();
serde_json::to_string(&json).map_err(HashError::Serialization)
}
pub fn canonical_value<T: Serialize + ?Sized>(value: &T) -> Result<serde_json::Value, HashError> {
let mut json = serde_json::to_value(value).map_err(HashError::Serialization)?;
json.sort_all_objects();
Ok(json)
}
#[must_use]
pub fn digest_hex(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
pub fn canonical_digest<T: Serialize + ?Sized>(value: &T) -> Result<Digest, HashError> {
let json = canonical_json(value)?;
Ok(Digest(digest_hex(json.as_bytes())))
}
#[must_use]
pub fn derive_uuid(domain: &str, parts: &[&str]) -> Uuid {
let mut hasher = blake3::Hasher::new();
hasher.update(domain.as_bytes());
for part in parts {
hasher.update(b"\0");
hasher.update(part.as_bytes());
}
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
uuid::Builder::from_custom_bytes(bytes).into_uuid()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn canonical_json_sorts_keys_recursively() {
let value = json!({"b": {"z": 1, "a": [{"y": 1, "x": 2}]}, "a": 1});
assert_eq!(
canonical_json(&value).unwrap(),
r#"{"a":1,"b":{"a":[{"x":2,"y":1}],"z":1}}"#
);
}
#[test]
fn derive_uuid_is_stable_domain_separated_and_injective() {
let a = derive_uuid("turnframe.test.v1", &["x", "y"]);
assert_eq!(a, derive_uuid("turnframe.test.v1", &["x", "y"]));
assert_ne!(a, derive_uuid("turnframe.other.v1", &["x", "y"]));
assert_ne!(a, derive_uuid("turnframe.test.v1", &["xy"]));
assert_ne!(a, derive_uuid("turnframe.test.v1", &["x", "y", "z"]));
assert_eq!(a.get_version_num(), 8, "custom, not a time-ordered v7");
}
#[test]
fn digest_is_stable_and_order_independent() {
let a = canonical_digest(&json!({"x": 1, "y": 2})).unwrap();
let b = canonical_digest(&json!({"y": 2, "x": 1})).unwrap();
let c = canonical_digest(&json!({"y": 2, "x": 3})).unwrap();
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(a.as_str().len(), 64);
}
}