acton_ern/model/
sha1_name.rs

1use std::fmt;
2use std::hash::Hash;
3
4use derive_more::{AsRef, From, Into};
5use mti::prelude::*;
6
7use crate::errors::ErnError;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12/// Represents a content-addressable identifier in an Entity Resource Name (ERN).
13///
14/// `SHA1Name` uses the UUID v5 algorithm (based on SHA1 hash) to generate
15/// deterministic, content-addressable identifiers. Unlike `EntityRoot` which
16/// generates different IDs for the same input (incorporating timestamps),
17/// `SHA1Name` will always generate the same ID for the same input content.
18///
19/// This makes `SHA1Name` ideal for:
20/// - Content-addressable resources where the same content should have the same identifier
21/// - Deterministic resource naming where reproducibility is important
22/// - Scenarios where you want to avoid duplicate resources with the same content
23#[derive(AsRef, From, Into, Eq, Debug, PartialEq, Clone, Hash, Default, PartialOrd)]
24pub struct SHA1Name {
25    /// The unique identifier for this entity, generated using the `mti` crate's
26    /// `MagicTypeId` type with UUID v5 algorithm.
27    name: MagicTypeId,
28}
29
30impl SHA1Name {
31    /// Returns a reference to the underlying `MagicTypeId`.
32    ///
33    /// This is useful when you need to access the raw identifier for
34    /// comparison or other operations.
35    ///
36    /// # Example
37    ///
38    /// ```
39    /// # use acton_ern::prelude::*;
40    /// # fn example() -> Result<(), ErnError> {
41    /// let name1 = SHA1Name::new("document-content".to_string())?;
42    /// let name2 = SHA1Name::new("document-content".to_string())?;
43    ///
44    /// // Same content produces the same ID
45    /// assert_eq!(name1.name(), name2.name());
46    /// # Ok(())
47    /// # }
48    /// ```
49    pub fn name(&self) -> &MagicTypeId {
50        &self.name
51    }
52    
53    /// Returns the string representation of this identifier.
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// # use acton_ern::prelude::*;
59    /// # fn example() -> Result<(), ErnError> {
60    /// let name = SHA1Name::new("document-content".to_string())?;
61    /// let id_str = name.as_str();
62    ///
63    /// // The string will be a deterministic ID based on the content
64    /// println!("SHA1 ID: {}", id_str);
65    /// # Ok(())
66    /// # }
67    /// ```
68    pub fn as_str(&self) -> &str {
69        &self.name
70    }
71
72    /// Creates a new `SHA1Name` with the given value.
73    ///
74    /// This method generates a deterministic, content-addressable identifier using
75    /// the UUID v5 algorithm based on SHA1 hash. Unlike `EntityRoot`, the same input
76    /// value will always produce the same ID, making it suitable for content-addressable
77    /// resources.
78    ///
79    /// # Arguments
80    ///
81    /// * `value` - The string value to use for generating the SHA1 hash
82    ///
83    /// # Validation Rules
84    ///
85    /// * Value cannot be empty
86    /// * Value must be between 1 and 1024 characters
87    ///
88    /// # Returns
89    ///
90    /// * `Ok(SHA1Name)` - If validation passes
91    /// * `Err(ErnError)` - If validation fails
92    ///
93    /// # Example
94    ///
95    /// ```
96    /// # use acton_ern::prelude::*;
97    /// # fn example() -> Result<(), ErnError> {
98    /// let name1 = SHA1Name::new("document-content".to_string())?;
99    /// let name2 = SHA1Name::new("document-content".to_string())?;
100    ///
101    /// // Same content produces the same ID
102    /// assert_eq!(name1.to_string(), name2.to_string());
103    /// # Ok(())
104    /// # }
105    /// ```
106    pub fn new(value: String) -> Result<Self, ErnError> {
107        // Check if empty
108        if value.is_empty() {
109            return Err(ErnError::ParseFailure("SHA1Name", "cannot be empty".to_string()));
110        }
111        
112        // Check length
113        if value.len() > 1024 {
114            return Err(ErnError::ParseFailure(
115                "SHA1Name",
116                format!("length exceeds maximum of 1024 characters (got {})", value.len())
117            ));
118        }
119        
120        Ok(SHA1Name {
121            name: value.create_type_id::<V5>(),
122        })
123    }
124}
125
126impl fmt::Display for SHA1Name {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        let id = &self.name;
129        write!(f, "{id}")
130    }
131}
132
133/// Implementation of `FromStr` for `SHA1Name` to create an entity from a string.
134impl std::str::FromStr for SHA1Name {
135    type Err = ErnError;
136    
137    /// Creates a `SHA1Name` from a string.
138    ///
139    /// This method generates a deterministic, content-addressable identifier using
140    /// the UUID v5 algorithm based on SHA1 hash. The same input string will always
141    /// produce the same ID.
142    ///
143    /// # Arguments
144    ///
145    /// * `s` - The string value to use for generating the SHA1 hash
146    ///
147    /// # Returns
148    ///
149    /// * `Ok(SHA1Name)` - If validation passes
150    /// * `Err(ErnError)` - If validation fails
151    ///
152    /// # Example
153    ///
154    /// ```
155    /// # use acton_ern::prelude::*;
156    /// # use std::str::FromStr;
157    /// # fn example() -> Result<(), ErnError> {
158    /// let name1 = SHA1Name::from_str("document-content")?;
159    /// let name2 = SHA1Name::new("document-content".to_string())?;
160    ///
161    /// // FromStr and new() produce the same result for the same input
162    /// assert_eq!(name1.to_string(), name2.to_string());
163    /// # Ok(())
164    /// # }
165    /// ```
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        // Check if empty
168        if s.is_empty() {
169            return Err(ErnError::ParseFailure("SHA1Name", "cannot be empty".to_string()));
170        }
171        
172        // Check length
173        if s.len() > 1024 {
174            return Err(ErnError::ParseFailure(
175                "SHA1Name",
176                format!("length exceeds maximum of 1024 characters (got {})", s.len())
177            ));
178        }
179        
180        Ok(SHA1Name {
181            name: s.create_type_id::<V5>(),
182        })
183    }
184}
185
186#[cfg(feature = "serde")]
187impl Serialize for SHA1Name {
188    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
189    where
190        S: Serializer,
191    {
192        // Serialize the MagicTypeId as a string
193        serializer.serialize_str(self.name.as_ref())
194    }
195}
196
197#[cfg(feature = "serde")]
198impl<'de> Deserialize<'de> for SHA1Name {
199    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
200    where
201        D: Deserializer<'de>,
202    {
203        // Deserialize as a string, then create a new SHA1Name
204        let s = String::deserialize(deserializer)?;
205        SHA1Name::new(s).map_err(serde::de::Error::custom)
206    }
207}
208
209use crate::traits::ErnComponent;
210use crate::Part;
211
212impl ErnComponent for SHA1Name {
213    fn prefix() -> &'static str {
214        ""
215    }
216    type NextState = Part;
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use std::str::FromStr;
223
224    #[test]
225    fn test_sha1name_deterministic() {
226        // Same input should produce the same SHA1Name
227        let name1 = SHA1Name::new("test_content".to_string()).unwrap();
228        let name2 = SHA1Name::new("test_content".to_string()).unwrap();
229        
230        assert_eq!(name1, name2);
231        assert_eq!(name1.to_string(), name2.to_string());
232    }
233
234    #[test]
235    fn test_sha1name_different_inputs() {
236        // Different inputs with more distinct content should produce different SHA1Names
237        let name1 = SHA1Name::new("completely_different_content_1".to_string()).unwrap();
238        let name2 = SHA1Name::new("entirely_unique_content_2".to_string()).unwrap();
239        
240        assert_ne!(name1, name2);
241        assert_ne!(name1.to_string(), name2.to_string());
242    }
243
244    #[test]
245    fn test_sha1name_from_str() {
246        // FromStr should produce the same result as new()
247        let name1 = SHA1Name::new("test_content".to_string()).unwrap();
248        let name2 = SHA1Name::from_str("test_content").unwrap();
249        
250        assert_eq!(name1, name2);
251        assert_eq!(name1.to_string(), name2.to_string());
252    }
253
254    #[test]
255    fn test_sha1name_display() {
256        let name = SHA1Name::new("test_content".to_string()).unwrap();
257        
258        // The string representation should contain the input value
259        let display = name.to_string();
260        assert!(!display.is_empty());
261    }
262    #[test]
263    fn test_sha1name_validation_empty() {
264        let result = SHA1Name::new("".to_string());
265        assert!(result.is_err());
266        match result {
267            Err(ErnError::ParseFailure(component, msg)) => {
268                assert_eq!(component, "SHA1Name");
269                assert!(msg.contains("empty"));
270            }
271            _ => panic!("Expected ParseFailure error for empty SHA1Name"),
272        }
273    }
274    
275    #[test]
276    fn test_sha1name_validation_too_long() {
277        let long_value = "a".repeat(1025);
278        let result = SHA1Name::new(long_value);
279        assert!(result.is_err());
280        match result {
281            Err(ErnError::ParseFailure(component, msg)) => {
282                assert_eq!(component, "SHA1Name");
283                assert!(msg.contains("length exceeds maximum"));
284            }
285            _ => panic!("Expected ParseFailure error for too long SHA1Name"),
286        }
287    }
288    
289    #[test]
290    fn test_sha1name_from_str_validation() {
291        let result = SHA1Name::from_str("");
292        assert!(result.is_err());
293        match result {
294            Err(ErnError::ParseFailure(component, msg)) => {
295                assert_eq!(component, "SHA1Name");
296                assert!(msg.contains("empty"));
297            }
298            _ => panic!("Expected ParseFailure error for empty SHA1Name from_str"),
299        }
300    }
301}