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}