Skip to main content

gonzalo_core/
revision.rs

1//! Content hashing and per-record revisions for optimistic concurrency.
2
3use serde::{Deserialize, Serialize};
4
5/// A content hash (blake3, hex-encoded) of a record body.
6#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
7pub struct ContentHash(pub String);
8
9impl ContentHash {
10    pub fn of(bytes: &[u8]) -> Self {
11        Self(blake3::hash(bytes).to_hex().to_string())
12    }
13}
14
15/// A record revision: a monotonic counter plus the body's content hash.
16/// Two writers diverge when their `counter`/`hash` pair differs.
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Revision {
19    pub counter: u64,
20    pub hash: ContentHash,
21}
22
23impl Revision {
24    /// The first revision for a freshly created record body.
25    pub fn initial(body: &[u8]) -> Self {
26        Self {
27            counter: 0,
28            hash: ContentHash::of(body),
29        }
30    }
31
32    /// The next revision after `self` for an updated body.
33    pub fn next(&self, body: &[u8]) -> Self {
34        Self {
35            counter: self.counter + 1,
36            hash: ContentHash::of(body),
37        }
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn hash_is_stable_and_distinct() {
47        assert_eq!(ContentHash::of(b"abc"), ContentHash::of(b"abc"));
48        assert_ne!(ContentHash::of(b"abc"), ContentHash::of(b"abd"));
49    }
50
51    #[test]
52    fn next_increments_counter_and_rehashes() {
53        let r0 = Revision::initial(b"v1");
54        let r1 = r0.next(b"v2");
55        assert_eq!(r0.counter, 0);
56        assert_eq!(r1.counter, 1);
57        assert_ne!(r0.hash, r1.hash);
58    }
59}