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}