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/// Interprets `value` as a root identifier.
13///
14/// A value that is already a fully-formed `MagicTypeId` (such as the
15/// `pool_01kytrwjv4eb1rt080sp4txr5t` found in an ERN's `Display` output) is preserved
16/// verbatim. Anything else is treated as a bare name and receives a freshly minted,
17/// time-ordered v7 suffix.
18///
19/// Preserving formed identifiers is what makes parsing idempotent: without it, every
20/// parse folds the previous suffix into the prefix and mints another one, so repeated
21/// parse cycles accumulate garbage and an ERN never round-trips through its own
22/// `Display` output.
23fn preserve_or_mint(value: &str) -> MagicTypeId {
24    value
25        .parse::<MagicTypeId>()
26        .unwrap_or_else(|_| value.create_type_id::<V7>())
27}
28
29/// Represents the root component in an Entity Resource Name (ERN).
30///
31/// The root component is a unique identifier for the base resource in the ERN hierarchy.
32/// It uses the `mti` crate's `MagicTypeId` with UUID v7 algorithm to generate
33/// time-ordered, unique identifiers that enable k-sortability.
34///
35/// When using `EntityRoot`, each call to create a new root from a bare *name* will
36/// generate a different ID, as it incorporates the current timestamp. This makes
37/// `EntityRoot` suitable for resources that should be ordered by creation time.
38/// A value that is already a fully-formed identifier (for example one taken from an
39/// existing ERN) is preserved as-is rather than reissued, so ERNs round-trip through
40/// parsing and serialization unchanged.
41///
42/// For content-addressable, deterministic IDs, use `SHA1Name` instead.
43#[derive(AsRef, From, Into, Eq, Debug, PartialEq, Clone, Hash, Default, PartialOrd)]
44pub struct EntityRoot {
45    /// The unique identifier for this root entity, generated using the `mti` crate's
46    /// `MagicTypeId` type.
47    name: MagicTypeId,
48}
49
50impl EntityRoot {
51    /// Returns a reference to the underlying `MagicTypeId`.
52    ///
53    /// This is useful when you need to access the raw identifier for
54    /// comparison or sorting operations.
55    ///
56    /// # Example
57    ///
58    /// ```
59    /// # use acton_ern::prelude::*;
60    /// # fn example() -> Result<(), ErnError> {
61    /// let root1 = EntityRoot::new("resource1".to_string())?;
62    /// let root2 = EntityRoot::new("resource2".to_string())?;
63    ///
64    /// // Compare roots by their MagicTypeId
65    /// let comparison = root1.name().cmp(root2.name());
66    /// # Ok(())
67    /// # }
68    /// ```
69    pub fn name(&self) -> &MagicTypeId {
70        &self.name
71    }
72
73    /// Returns the string representation of this root's identifier.
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// # use acton_ern::prelude::*;
79    /// # fn example() -> Result<(), ErnError> {
80    /// let root = EntityRoot::new("profile".to_string())?;
81    /// let id_str = root.as_str();
82    ///
83    /// // The string will contain the original name followed by a timestamp-based suffix
84    /// assert!(id_str.starts_with("profile_"));
85    /// # Ok(())
86    /// # }
87    /// ```
88    pub fn as_str(&self) -> &str {
89        &self.name
90    }
91
92    /// Returns the human-readable name of this root, without the generated suffix.
93    ///
94    /// Where [`as_str`](Self::as_str) yields the full identifier (`worker_01h455vb4pex…`),
95    /// this yields just the name it was created from (`worker`). That name is stable across
96    /// roots minted from the same input, which makes it the right value to derive a
97    /// deterministic child path from:
98    ///
99    /// ```
100    /// # use acton_ern::prelude::*;
101    /// # fn example() -> Result<(), ErnError> {
102    /// let parent = Ern::with_root("pool")?;
103    /// let requested = Ern::with_root("worker")?;
104    ///
105    /// // Same child every time, regardless of when `requested` was minted
106    /// let child = parent.add_part(requested.name())?;
107    /// assert_eq!(child, parent.add_part(requested.name())?);
108    /// # Ok(())
109    /// # }
110    /// ```
111    ///
112    /// Returns an empty string for a root that carries no prefix, such as one built from a
113    /// bare suffix or [`EntityRoot::default`].
114    ///
115    /// # Example
116    ///
117    /// ```
118    /// # use acton_ern::prelude::*;
119    /// # fn example() -> Result<(), ErnError> {
120    /// let root = EntityRoot::new("profile".to_string())?;
121    ///
122    /// assert_eq!(root.name_str(), "profile");
123    /// assert!(root.as_str().starts_with("profile_"));
124    /// # Ok(())
125    /// # }
126    /// ```
127    pub fn name_str(&self) -> &str {
128        self.name.prefix().as_str()
129    }
130
131    /// Creates a new `EntityRoot` with the given value.
132    ///
133    /// When `value` is a bare name, this method generates a time-ordered, unique identifier
134    /// using the UUID v7 algorithm. Each call with the same name will generate a different ID,
135    /// as it incorporates the current timestamp. This makes `EntityRoot` suitable for
136    /// resources that should be ordered by creation time.
137    ///
138    /// When `value` is already a fully-formed identifier, it is preserved verbatim so that
139    /// existing roots survive a parse or deserialization round trip.
140    ///
141    /// # Arguments
142    ///
143    /// * `value` - The string value to use as the base for the entity root ID
144    ///
145    /// # Validation Rules
146    ///
147    /// * Value cannot be empty
148    /// * Value must be between 1 and 255 characters
149    ///
150    /// # Returns
151    ///
152    /// * `Ok(EntityRoot)` - If validation passes
153    /// * `Err(ErnError)` - If validation fails
154    ///
155    /// # Example
156    ///
157    /// ```
158    /// # use acton_ern::prelude::*;
159    /// # fn example() -> Result<(), ErnError> {
160    /// let root = EntityRoot::new("profile".to_string())?;
161    ///
162    /// // The ID will contain the original name followed by a timestamp-based suffix
163    /// assert!(root.to_string().starts_with("profile_"));
164    ///
165    /// // Re-creating from a formed identifier preserves it
166    /// let same = EntityRoot::new(root.to_string())?;
167    /// assert_eq!(root, same);
168    /// # Ok(())
169    /// # }
170    /// ```
171    pub fn new(value: String) -> Result<Self, ErnError> {
172        // Check if empty
173        if value.is_empty() {
174            return Err(ErnError::ParseFailure(
175                "EntityRoot",
176                "cannot be empty".to_string(),
177            ));
178        }
179
180        // Check length
181        if value.len() > 255 {
182            return Err(ErnError::ParseFailure(
183                "EntityRoot",
184                format!(
185                    "length exceeds maximum of 255 characters (got {})",
186                    value.len()
187                ),
188            ));
189        }
190
191        Ok(EntityRoot {
192            name: preserve_or_mint(&value),
193        })
194    }
195}
196
197impl fmt::Display for EntityRoot {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        let id = &self.name;
200        write!(f, "{id}")
201    }
202}
203
204/// Implementation of `FromStr` for `EntityRoot` to create an entity root from a string.
205impl std::str::FromStr for EntityRoot {
206    type Err = ErnError;
207
208    /// Creates an `EntityRoot` from a string.
209    ///
210    /// A bare name receives a freshly minted, time-ordered v7 identifier, so each call with
211    /// the same name yields a different ID. A string that is already a fully-formed
212    /// identifier is preserved verbatim, which is what allows `ErnParser` to round-trip an
213    /// ERN's own `Display` output.
214    ///
215    /// # Arguments
216    ///
217    /// * `s` - The string value to use as the base for the entity root ID
218    ///
219    /// # Returns
220    ///
221    /// * `Ok(EntityRoot)` - If validation passes
222    /// * `Err(ErnError)` - If validation fails
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        // Check if empty
225        if s.is_empty() {
226            return Err(ErnError::ParseFailure(
227                "EntityRoot",
228                "cannot be empty".to_string(),
229            ));
230        }
231
232        // Check length
233        if s.len() > 255 {
234            return Err(ErnError::ParseFailure(
235                "EntityRoot",
236                format!("length exceeds maximum of 255 characters (got {})", s.len()),
237            ));
238        }
239
240        Ok(EntityRoot {
241            name: preserve_or_mint(s),
242        })
243    }
244}
245
246#[cfg(feature = "serde")]
247impl Serialize for EntityRoot {
248    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
249    where
250        S: Serializer,
251    {
252        // Serialize the MagicTypeId as a string
253        serializer.serialize_str(self.name.as_ref())
254    }
255}
256
257#[cfg(feature = "serde")]
258impl<'de> Deserialize<'de> for EntityRoot {
259    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
260    where
261        D: Deserializer<'de>,
262    {
263        // Deserialize as a string, then create a new EntityRoot
264        let s = String::deserialize(deserializer)?;
265        EntityRoot::new(s).map_err(serde::de::Error::custom)
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use std::str::FromStr;
273
274    #[test]
275    fn test_entity_root_creation() -> anyhow::Result<()> {
276        let root = EntityRoot::new("test-entity".to_string())?;
277        assert!(!root.to_string().is_empty());
278        Ok(())
279    }
280
281    #[test]
282    fn test_entity_root_uniqueness() -> anyhow::Result<()> {
283        // EntityRoot should generate different IDs for the same input (non-deterministic)
284        let root1 = EntityRoot::new("same-content".to_string())?;
285        let root2 = EntityRoot::new("same-content".to_string())?;
286
287        // The string representations should be different
288        assert_ne!(root1.to_string(), root2.to_string());
289        Ok(())
290    }
291
292    #[test]
293    fn test_entity_root_from_str() -> anyhow::Result<()> {
294        let root = EntityRoot::from_str("test-entity")?;
295        assert!(!root.to_string().is_empty());
296        Ok(())
297    }
298
299    #[test]
300    fn test_entity_root_validation_empty() {
301        let result = EntityRoot::new("".to_string());
302        assert!(result.is_err());
303        match result {
304            Err(ErnError::ParseFailure(component, msg)) => {
305                assert_eq!(component, "EntityRoot");
306                assert!(msg.contains("empty"));
307            }
308            _ => panic!("Expected ParseFailure error for empty EntityRoot"),
309        }
310    }
311
312    #[test]
313    fn test_entity_root_validation_too_long() {
314        let long_value = "a".repeat(256);
315        let result = EntityRoot::new(long_value);
316        assert!(result.is_err());
317        match result {
318            Err(ErnError::ParseFailure(component, msg)) => {
319                assert_eq!(component, "EntityRoot");
320                assert!(msg.contains("length exceeds maximum"));
321            }
322            _ => panic!("Expected ParseFailure error for too long EntityRoot"),
323        }
324    }
325
326    #[test]
327    fn test_entity_root_preserves_formed_identifier() -> anyhow::Result<()> {
328        // Re-reading a root's own string representation must reproduce it exactly,
329        // rather than folding the suffix into the prefix and minting a new one.
330        let root = EntityRoot::new("pool".to_string())?;
331        let reread = EntityRoot::from_str(root.as_str())?;
332
333        assert_eq!(root, reread);
334        assert_eq!(root.to_string(), reread.to_string());
335        Ok(())
336    }
337
338    #[test]
339    fn test_entity_root_parsing_is_idempotent() -> anyhow::Result<()> {
340        // Repeated round trips must converge, not accumulate prefix garbage.
341        let mut root = EntityRoot::new("pool".to_string())?;
342        let expected = root.to_string();
343
344        for _ in 0..5 {
345            root = EntityRoot::from_str(root.as_str())?;
346            assert_eq!(root.to_string(), expected);
347        }
348        Ok(())
349    }
350
351    #[test]
352    fn test_entity_root_bare_name_still_mints() -> anyhow::Result<()> {
353        // A name that merely contains an underscore is not a formed identifier.
354        let root1 = EntityRoot::from_str("root_a")?;
355        let root2 = EntityRoot::from_str("root_a")?;
356
357        assert_ne!(root1, root2);
358        assert!(root1.as_str().starts_with("root_a_"));
359        Ok(())
360    }
361
362    #[test]
363    fn test_entity_root_from_str_validation() {
364        let result = EntityRoot::from_str("");
365        assert!(result.is_err());
366        match result {
367            Err(ErnError::ParseFailure(component, msg)) => {
368                assert_eq!(component, "EntityRoot");
369                assert!(msg.contains("empty"));
370            }
371            _ => panic!("Expected ParseFailure error for empty EntityRoot from_str"),
372        }
373    }
374}