p2panda_rs/document/
document_id.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::fmt::Display;
4use std::hash::Hash as StdHash;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9use crate::document::error::DocumentIdError;
10use crate::hash::{Hash, HashId};
11use crate::operation::OperationId;
12use crate::{Human, Validate};
13
14/// Identifier of a document.
15///
16/// Documents are formed by one or many operations which create, update or delete the regarding
17/// document. The whole document is always identified by the [`OperationId`] of its initial
18/// `CREATE` operation. This operation id is equivalent to the [`Hash`](crate::hash::Hash) of the
19/// entry with which that operation was published.
20///
21/// ```text
22/// The document with the following operation graph has the id "2fa..":
23///
24/// [CREATE] (Hash: "2fa..") <-- [UPDATE] (Hash: "de8..") <-- [UPDATE] (Hash: "89c..")
25///                         \
26///                          \__ [UPDATE] (Hash: "eff..")
27/// ```
28#[derive(Clone, Debug, StdHash, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
29pub struct DocumentId(OperationId);
30
31impl DocumentId {
32    /// Creates a new instance of `DocumentId`.
33    pub fn new(id: &OperationId) -> Self {
34        Self(id.to_owned())
35    }
36
37    /// Returns the string representation of the document id.
38    pub fn as_str(&self) -> &str {
39        self.0.as_str()
40    }
41}
42
43impl HashId for DocumentId {
44    /// Access the inner [`crate::hash::Hash`] value of this document id.
45    fn as_hash(&self) -> &Hash {
46        self.0.as_hash()
47    }
48}
49
50impl Display for DocumentId {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{}", self.0.as_str())
53    }
54}
55
56impl Human for DocumentId {
57    fn display(&self) -> String {
58        let offset = yasmf_hash::MAX_YAMF_HASH_SIZE * 2 - 6;
59        format!("<DocumentId {}>", &self.0.as_str()[offset..])
60    }
61}
62
63impl Validate for DocumentId {
64    type Error = DocumentIdError;
65
66    fn validate(&self) -> Result<(), Self::Error> {
67        self.0.validate()?;
68        Ok(())
69    }
70}
71
72impl From<Hash> for DocumentId {
73    fn from(hash: Hash) -> Self {
74        Self(hash.into())
75    }
76}
77
78impl FromStr for DocumentId {
79    type Err = DocumentIdError;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        Ok(Self(s.parse::<OperationId>()?))
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use std::str::FromStr;
89
90    use ciborium::cbor;
91    use rstest::rstest;
92
93    use crate::hash::Hash;
94    use crate::operation::OperationId;
95    use crate::serde::{deserialize_into, hex_string_to_bytes, serialize_from, serialize_value};
96    use crate::test_utils::fixtures::random_hash;
97    use crate::Human;
98
99    use super::DocumentId;
100
101    #[rstest]
102    fn conversion(#[from(random_hash)] hash: Hash) {
103        // Converts any string to `DocumentId`
104        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
105        let document_id: DocumentId = hash_str.parse().unwrap();
106        assert_eq!(
107            document_id,
108            DocumentId::new(&hash_str.parse::<OperationId>().unwrap())
109        );
110
111        // Converts any `Hash` to `DocumentId`
112        let document_id = DocumentId::from(hash.clone());
113        assert_eq!(document_id, DocumentId::new(&hash.into()));
114
115        // Fails when string is not a hash
116        assert!("This is not a hash".parse::<DocumentId>().is_err());
117    }
118
119    #[test]
120    fn string_representation() {
121        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
122        let document_id: DocumentId = hash_str.parse().unwrap();
123
124        assert_eq!(document_id.to_string(), hash_str);
125        assert_eq!(document_id.as_str(), hash_str);
126        assert_eq!(format!("{}", document_id), hash_str);
127    }
128
129    #[test]
130    fn short_representation() {
131        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
132        let document_id: DocumentId = hash_str.parse().unwrap();
133
134        assert_eq!(document_id.display(), "<DocumentId 6ec805>");
135    }
136
137    #[test]
138    fn serialize() {
139        let bytes = serialize_from(
140            DocumentId::from_str(
141                "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805",
142            )
143            .unwrap(),
144        );
145        assert_eq!(
146            bytes,
147            vec![
148                88, 34, 0, 32, 207, 176, 250, 55, 243, 109, 8, 47, 170, 211, 136, 106, 159, 251,
149                204, 40, 19, 183, 175, 233, 15, 6, 9, 165, 86, 212, 37, 241, 167, 110, 200, 5
150            ]
151        );
152    }
153
154    #[test]
155    fn deserialize() {
156        let hash_str = "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805";
157        let document_id: DocumentId =
158            deserialize_into(&serialize_value(cbor!(hex_string_to_bytes(
159                "0020cfb0fa37f36d082faad3886a9ffbcc2813b7afe90f0609a556d425f1a76ec805"
160            ))))
161            .unwrap();
162        assert_eq!(DocumentId::from_str(hash_str).unwrap(), document_id);
163
164        // Invalid hashes
165        let invalid_hash = deserialize_into::<DocumentId>(&serialize_value(cbor!("1234")));
166        assert!(invalid_hash.is_err());
167        let empty_hash = deserialize_into::<DocumentId>(&serialize_value(cbor!("")));
168        assert!(empty_hash.is_err());
169    }
170}