schema_core/config/
content_hash.rs1use std::hash::{Hash, Hasher};
2
3const FNV_OFFSET: u32 = 2_166_136_261;
5const FNV_PRIME: u32 = 16_777_619;
7
8#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
9pub struct ContentHash(u32);
10
11impl ContentHash {
12 pub fn new(value: u32) -> Self {
13 Self(value)
14 }
15
16 pub fn of<T: Hash>(value: &T) -> Self {
20 let mut hasher = Fnv1aHasher::default();
21 value.hash(&mut hasher);
22 Self(hasher.0)
23 }
24}
25
26impl std::fmt::Display for ContentHash {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "{:08x}", self.0)
29 }
30}
31
32impl serde::Serialize for ContentHash {
35 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
36 serializer.collect_str(self)
37 }
38}
39
40struct Fnv1aHasher(u32);
43
44impl Default for Fnv1aHasher {
45 fn default() -> Self {
46 Self(FNV_OFFSET)
47 }
48}
49
50impl Hasher for Fnv1aHasher {
51 fn finish(&self) -> u64 {
52 u64::from(self.0)
53 }
54
55 fn write(&mut self, bytes: &[u8]) {
56 for byte in bytes {
57 self.0 ^= u32::from(*byte);
58 self.0 = self.0.wrapping_mul(FNV_PRIME);
59 }
60 }
61}
62
63#[cfg(test)]
64#[allow(clippy::unwrap_used)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn hash_is_deterministic_for_equal_values() {
70 let a = ContentHash::of(&("users", 1u8, vec!["id", "email"]));
71 let b = ContentHash::of(&("users", 1u8, vec!["id", "email"]));
72 assert_eq!(a, b);
73 }
74
75 #[test]
76 fn hash_changes_when_structure_changes() {
77 let before = ContentHash::of(&vec!["id", "email"]);
78 let after = ContentHash::of(&vec!["id", "email", "name"]);
79 assert_ne!(before, after);
80 }
81
82 #[test]
83 fn display_is_eight_hex_digits() {
84 assert_eq!(format!("{}", ContentHash::new(0xABCD)), "0000abcd");
85 }
86}