p2panda_rs/operation/
operation_id.rs

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