1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use std::fmt::{Debug, Display, Write};

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

/// Information about a `Document`'s revision history.
#[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub struct Revision {
    /// The current revision id of the document. This value is sequentially incremented on each document update.
    pub id: u32,

    /// The SHA256 digest of the bytes contained within the `Document`.
    pub sha256: [u8; 32],
}

impl Revision {
    /// Creates the first revision for a document with the SHA256 digest of the passed bytes.
    #[must_use]
    pub fn new(contents: &[u8]) -> Self {
        Self::with_id(0, contents)
    }

    /// Creates a revision with `id` for a document with the SHA256 digest of the passed bytes.
    #[must_use]
    pub fn with_id(id: u32, contents: &[u8]) -> Self {
        Self {
            id,
            sha256: digest(contents),
        }
    }

    /// Creates the next revision in sequence with an updated digest. If the digest doesn't change, None is returned.
    ///
    /// # Panics
    ///
    /// Panics if `id` overflows.
    #[must_use]
    pub fn next_revision(&self, new_contents: &[u8]) -> Option<Self> {
        let sha256 = digest(new_contents);
        if sha256 == self.sha256 {
            None
        } else {
            Some(Self {
                id: self
                    .id
                    .checked_add(1)
                    .expect("need to implement revision id wrapping or increase revision id size"),
                sha256,
            })
        }
    }
}

impl Debug for Revision {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Revision({self})")
    }
}

impl Display for Revision {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        <u32 as Display>::fmt(&self.id, f)?;
        f.write_char('-')?;
        for byte in self.sha256 {
            f.write_fmt(format_args!("{byte:02x}"))?;
        }
        Ok(())
    }
}

fn digest(payload: &[u8]) -> [u8; 32] {
    let mut hasher = Sha256::default();
    hasher.update(payload);
    hasher.finalize().into()
}

#[test]
fn revision_tests() {
    let original_contents = b"one";
    let first_revision = Revision::new(original_contents);
    let original_digest =
        hex_literal::hex!("7692c3ad3540bb803c020b3aee66cd8887123234ea0c6e7143c0add73ff431ed");
    assert_eq!(
        first_revision,
        Revision {
            id: 0,
            sha256: original_digest
        }
    );
    assert!(first_revision.next_revision(original_contents).is_none());

    let updated_contents = b"two";
    let next_revision = first_revision
        .next_revision(updated_contents)
        .expect("new contents should create a new revision");
    assert_eq!(
        next_revision,
        Revision {
            id: 1,
            sha256: hex_literal::hex!(
                "3fc4ccfe745870e2c0d99f71f30ff0656c8dedd41cc1d7d3d376b0dbe685e2f3"
            )
        }
    );
    assert!(next_revision.next_revision(updated_contents).is_none());

    assert_eq!(
        next_revision.next_revision(original_contents),
        Some(Revision {
            id: 2,
            sha256: original_digest
        })
    );
}

#[test]
fn revision_display_test() {
    let original_contents = b"one";
    let first_revision = Revision::new(original_contents);
    assert_eq!(
        first_revision.to_string(),
        "0-7692c3ad3540bb803c020b3aee66cd8887123234ea0c6e7143c0add73ff431ed"
    );
    assert_eq!(
        format!("{first_revision:?}"),
        "Revision(0-7692c3ad3540bb803c020b3aee66cd8887123234ea0c6e7143c0add73ff431ed)"
    );
}