acton_ern/model/
root.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 the root component in an Entity Resource Name (ERN).
13///
14/// The root component is a unique identifier for the base resource in the ERN hierarchy.
15/// It uses the `mti` crate's `MagicTypeId` with UUID v7 algorithm to generate
16/// time-ordered, unique identifiers that enable k-sortability.
17///
18/// When using `EntityRoot`, each call to create a new root with the same name will
19/// generate a different ID, as it incorporates the current timestamp. This makes
20/// `EntityRoot` suitable for resources that should be ordered by creation time.
21///
22/// For content-addressable, deterministic IDs, use `SHA1Name` instead.
23#[derive(AsRef, From, Into, Eq, Debug, PartialEq, Clone, Hash, Default, PartialOrd)]
24pub struct EntityRoot {
25    /// The unique identifier for this root entity, generated using the `mti` crate's
26    /// `MagicTypeId` type.
27    name: MagicTypeId,
28}
29
30impl EntityRoot {
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 sorting operations.
35    ///
36    /// # Example
37    ///
38    /// ```
39    /// # use acton_ern::prelude::*;
40    /// # fn example() -> Result<(), ErnError> {
41    /// let root1 = EntityRoot::new("resource1".to_string())?;
42    /// let root2 = EntityRoot::new("resource2".to_string())?;
43    ///
44    /// // Compare roots by their MagicTypeId
45    /// let comparison = root1.name().cmp(root2.name());
46    /// # Ok(())
47    /// # }
48    /// ```
49    pub fn name(&self) -> &MagicTypeId {
50        &self.name
51    }
52    
53    /// Returns the string representation of this root's identifier.
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// # use acton_ern::prelude::*;
59    /// # fn example() -> Result<(), ErnError> {
60    /// let root = EntityRoot::new("profile".to_string())?;
61    /// let id_str = root.as_str();
62    ///
63    /// // The string will contain the original name followed by a timestamp-based suffix
64    /// assert!(id_str.starts_with("profile_"));
65    /// # Ok(())
66    /// # }
67    /// ```
68    pub fn as_str(&self) -> &str {
69        &self.name
70    }
71
72    /// Creates a new `EntityRoot` with the given value.
73    ///
74    /// This method generates a time-ordered, unique identifier using the UUID v7 algorithm.
75    /// Each call to this method with the same input value will generate a different ID,
76    /// as it incorporates the current timestamp. This makes `EntityRoot` suitable for
77    /// resources that should be ordered by creation time.
78    ///
79    /// # Arguments
80    ///
81    /// * `value` - The string value to use as the base for the entity root ID
82    ///
83    /// # Validation Rules
84    ///
85    /// * Value cannot be empty
86    /// * Value must be between 1 and 255 characters
87    ///
88    /// # Returns
89    ///
90    /// * `Ok(EntityRoot)` - 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 root = EntityRoot::new("profile".to_string())?;
99    ///
100    /// // The ID will contain the original name followed by a timestamp-based suffix
101    /// assert!(root.to_string().starts_with("profile_"));
102    /// # Ok(())
103    /// # }
104    /// ```
105    pub fn new(value: String) -> Result<Self, ErnError> {
106        // Check if empty
107        if value.is_empty() {
108            return Err(ErnError::ParseFailure("EntityRoot", "cannot be empty".to_string()));
109        }
110        
111        // Check length
112        if value.len() > 255 {
113            return Err(ErnError::ParseFailure(
114                "EntityRoot",
115                format!("length exceeds maximum of 255 characters (got {})", value.len())
116            ));
117        }
118        
119        Ok(EntityRoot {
120            name: value.create_type_id::<V7>(),
121        })
122    }
123}
124
125impl fmt::Display for EntityRoot {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        let id = &self.name;
128        write!(f, "{id}")
129    }
130}
131
132/// Implementation of `FromStr` for `EntityRoot` to create an entity root from a string.
133impl std::str::FromStr for EntityRoot {
134    type Err = ErnError;
135    
136    /// Creates an `EntityRoot` from a string.
137    ///
138    /// This method generates a time-ordered, unique identifier using the UUID v7 algorithm.
139    /// Each call to this method with the same input string will generate a different ID,
140    /// as it incorporates the current timestamp.
141    ///
142    /// # Arguments
143    ///
144    /// * `s` - The string value to use as the base for the entity root ID
145    ///
146    /// # Returns
147    ///
148    /// * `Ok(EntityRoot)` - If validation passes
149    /// * `Err(ErnError)` - If validation fails
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        // Check if empty
152        if s.is_empty() {
153            return Err(ErnError::ParseFailure("EntityRoot", "cannot be empty".to_string()));
154        }
155        
156        // Check length
157        if s.len() > 255 {
158            return Err(ErnError::ParseFailure(
159                "EntityRoot",
160                format!("length exceeds maximum of 255 characters (got {})", s.len())
161            ));
162        }
163        
164        Ok(EntityRoot {
165            name: s.create_type_id::<V7>(),
166        })
167    }
168}
169
170#[cfg(feature = "serde")]
171impl Serialize for EntityRoot {
172    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173    where
174        S: Serializer,
175    {
176        // Serialize the MagicTypeId as a string
177        serializer.serialize_str(self.name.as_ref())
178    }
179}
180
181#[cfg(feature = "serde")]
182impl<'de> Deserialize<'de> for EntityRoot {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: Deserializer<'de>,
186    {
187        // Deserialize as a string, then create a new EntityRoot
188        let s = String::deserialize(deserializer)?;
189        EntityRoot::new(s).map_err(serde::de::Error::custom)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::str::FromStr;
197
198    #[test]
199    fn test_entity_root_creation() -> anyhow::Result<()> {
200        let root = EntityRoot::new("test-entity".to_string())?;
201        assert!(!root.to_string().is_empty());
202        Ok(())
203    }
204
205    #[test]
206    fn test_entity_root_uniqueness() -> anyhow::Result<()> {
207        // EntityRoot should generate different IDs for the same input (non-deterministic)
208        let root1 = EntityRoot::new("same-content".to_string())?;
209        let root2 = EntityRoot::new("same-content".to_string())?;
210        
211        // The string representations should be different
212        assert_ne!(root1.to_string(), root2.to_string());
213        Ok(())
214    }
215
216    #[test]
217    fn test_entity_root_from_str() -> anyhow::Result<()> {
218        let root = EntityRoot::from_str("test-entity")?;
219        assert!(!root.to_string().is_empty());
220        Ok(())
221    }
222
223    #[test]
224    fn test_entity_root_validation_empty() {
225        let result = EntityRoot::new("".to_string());
226        assert!(result.is_err());
227        match result {
228            Err(ErnError::ParseFailure(component, msg)) => {
229                assert_eq!(component, "EntityRoot");
230                assert!(msg.contains("empty"));
231            }
232            _ => panic!("Expected ParseFailure error for empty EntityRoot"),
233        }
234    }
235
236    #[test]
237    fn test_entity_root_validation_too_long() {
238        let long_value = "a".repeat(256);
239        let result = EntityRoot::new(long_value);
240        assert!(result.is_err());
241        match result {
242            Err(ErnError::ParseFailure(component, msg)) => {
243                assert_eq!(component, "EntityRoot");
244                assert!(msg.contains("length exceeds maximum"));
245            }
246            _ => panic!("Expected ParseFailure error for too long EntityRoot"),
247        }
248    }
249
250    #[test]
251    fn test_entity_root_from_str_validation() {
252        let result = EntityRoot::from_str("");
253        assert!(result.is_err());
254        match result {
255            Err(ErnError::ParseFailure(component, msg)) => {
256                assert_eq!(component, "EntityRoot");
257                assert!(msg.contains("empty"));
258            }
259            _ => panic!("Expected ParseFailure error for empty EntityRoot from_str"),
260        }
261    }
262}