Skip to main content

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(
109                "EntityRoot",
110                "cannot be empty".to_string(),
111            ));
112        }
113
114        // Check length
115        if value.len() > 255 {
116            return Err(ErnError::ParseFailure(
117                "EntityRoot",
118                format!(
119                    "length exceeds maximum of 255 characters (got {})",
120                    value.len()
121                ),
122            ));
123        }
124
125        Ok(EntityRoot {
126            name: value.create_type_id::<V7>(),
127        })
128    }
129}
130
131impl fmt::Display for EntityRoot {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        let id = &self.name;
134        write!(f, "{id}")
135    }
136}
137
138/// Implementation of `FromStr` for `EntityRoot` to create an entity root from a string.
139impl std::str::FromStr for EntityRoot {
140    type Err = ErnError;
141
142    /// Creates an `EntityRoot` from a string.
143    ///
144    /// This method generates a time-ordered, unique identifier using the UUID v7 algorithm.
145    /// Each call to this method with the same input string will generate a different ID,
146    /// as it incorporates the current timestamp.
147    ///
148    /// # Arguments
149    ///
150    /// * `s` - The string value to use as the base for the entity root ID
151    ///
152    /// # Returns
153    ///
154    /// * `Ok(EntityRoot)` - If validation passes
155    /// * `Err(ErnError)` - If validation fails
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        // Check if empty
158        if s.is_empty() {
159            return Err(ErnError::ParseFailure(
160                "EntityRoot",
161                "cannot be empty".to_string(),
162            ));
163        }
164
165        // Check length
166        if s.len() > 255 {
167            return Err(ErnError::ParseFailure(
168                "EntityRoot",
169                format!("length exceeds maximum of 255 characters (got {})", s.len()),
170            ));
171        }
172
173        Ok(EntityRoot {
174            name: s.create_type_id::<V7>(),
175        })
176    }
177}
178
179#[cfg(feature = "serde")]
180impl Serialize for EntityRoot {
181    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182    where
183        S: Serializer,
184    {
185        // Serialize the MagicTypeId as a string
186        serializer.serialize_str(self.name.as_ref())
187    }
188}
189
190#[cfg(feature = "serde")]
191impl<'de> Deserialize<'de> for EntityRoot {
192    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193    where
194        D: Deserializer<'de>,
195    {
196        // Deserialize as a string, then create a new EntityRoot
197        let s = String::deserialize(deserializer)?;
198        EntityRoot::new(s).map_err(serde::de::Error::custom)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::str::FromStr;
206
207    #[test]
208    fn test_entity_root_creation() -> anyhow::Result<()> {
209        let root = EntityRoot::new("test-entity".to_string())?;
210        assert!(!root.to_string().is_empty());
211        Ok(())
212    }
213
214    #[test]
215    fn test_entity_root_uniqueness() -> anyhow::Result<()> {
216        // EntityRoot should generate different IDs for the same input (non-deterministic)
217        let root1 = EntityRoot::new("same-content".to_string())?;
218        let root2 = EntityRoot::new("same-content".to_string())?;
219
220        // The string representations should be different
221        assert_ne!(root1.to_string(), root2.to_string());
222        Ok(())
223    }
224
225    #[test]
226    fn test_entity_root_from_str() -> anyhow::Result<()> {
227        let root = EntityRoot::from_str("test-entity")?;
228        assert!(!root.to_string().is_empty());
229        Ok(())
230    }
231
232    #[test]
233    fn test_entity_root_validation_empty() {
234        let result = EntityRoot::new("".to_string());
235        assert!(result.is_err());
236        match result {
237            Err(ErnError::ParseFailure(component, msg)) => {
238                assert_eq!(component, "EntityRoot");
239                assert!(msg.contains("empty"));
240            }
241            _ => panic!("Expected ParseFailure error for empty EntityRoot"),
242        }
243    }
244
245    #[test]
246    fn test_entity_root_validation_too_long() {
247        let long_value = "a".repeat(256);
248        let result = EntityRoot::new(long_value);
249        assert!(result.is_err());
250        match result {
251            Err(ErnError::ParseFailure(component, msg)) => {
252                assert_eq!(component, "EntityRoot");
253                assert!(msg.contains("length exceeds maximum"));
254            }
255            _ => panic!("Expected ParseFailure error for too long EntityRoot"),
256        }
257    }
258
259    #[test]
260    fn test_entity_root_from_str_validation() {
261        let result = EntityRoot::from_str("");
262        assert!(result.is_err());
263        match result {
264            Err(ErnError::ParseFailure(component, msg)) => {
265                assert_eq!(component, "EntityRoot");
266                assert!(msg.contains("empty"));
267            }
268            _ => panic!("Expected ParseFailure error for empty EntityRoot from_str"),
269        }
270    }
271}