acton_ern/model/
ern.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::fmt::{Display, Formatter};
4use std::hash::Hash;
5use std::ops::Add;
6
7use crate::{Account, Category, Domain, EntityRoot, ErnComponent, Part, Parts};
8use crate::errors::ErnError;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// Represents an Entity Resource Name (ERN), which uniquely identifies resources in distributed systems.
14///
15/// An ERN follows the URN format and has the structure:
16/// `ern:domain:category:account:root/path/to/resource`
17///
18/// Each component serves a specific purpose:
19/// - `domain`: Classifies the resource (e.g., internal, external, custom domains)
20/// - `category`: Specifies the service or category within the system
21/// - `account`: Identifies the owner or account responsible for the resource
22/// - `root`: A unique identifier for the root of the resource hierarchy
23/// - `parts`: Optional path-like structure showing the resource's position within the hierarchy
24///
25/// ERNs can be k-sortable when using `UnixTime` or `Timestamp` ID types, enabling
26/// efficient ordering and range queries.
27#[derive(Debug, PartialEq, Clone, Eq, Hash)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub struct Ern {
30    pub domain: Domain,
31    pub category: Category,
32    pub account: Account,
33    pub root: EntityRoot,
34    pub parts: Parts,
35}
36
37impl Ord for Ern {
38    fn cmp(&self, other: &Self) -> Ordering {
39        self.root.name().cmp(other.root.name())
40    }
41}
42
43impl PartialOrd for Ern {
44    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45        Some(self.cmp(other))
46    }
47}
48
49impl Display for Ern {
50    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
51        let mut display = format!(
52            "{}{}:{}:{}:{}",
53            Domain::prefix(),
54            self.domain,
55            self.category,
56            self.account,
57            self.root
58        );
59        if !self.parts.0.is_empty() {
60            display = format!("{}/{}", display, self.parts);
61        }
62        write!(f, "{}", display)
63    }
64}
65
66impl Add for Ern {
67    type Output = Ern;
68
69    fn add(self, rhs: Self) -> Self::Output {
70        let mut new_parts = self.parts.0;
71        new_parts.extend(rhs.parts.0);
72        Ern {
73            domain: self.domain,
74            category: self.category,
75            account: self.account,
76            root: self.root,
77            parts: Parts(new_parts),
78        }
79    }
80}
81
82impl Ern {
83    /// Creates a new ERN with the specified components.
84    ///
85    /// # Arguments
86    ///
87    /// * `domain` - The domain component
88    /// * `category` - The category component
89    /// * `account` - The account component
90    /// * `root` - The root component
91    /// * `parts` - The path parts component
92    ///
93    /// # Example
94    ///
95    /// ```
96    /// # use acton_ern::prelude::*;
97    /// # fn example() -> Result<(), ErnError> {
98    /// let ern = Ern::new(
99    ///     Domain::new("my-app")?,
100    ///     Category::new("users")?,
101    ///     Account::new("tenant123")?,
102    ///     EntityRoot::new("profile".to_string())?,
103    ///     Parts::new(vec![Part::new("settings")?]),
104    /// );
105    /// # Ok(())
106    /// # }
107    /// ```
108    pub fn new(
109        domain: Domain,
110        category: Category,
111        account: Account,
112        root: EntityRoot,
113        parts: Parts,
114    ) -> Self {
115        Ern {
116            domain,
117            category,
118            account,
119            root,
120            parts,
121        }
122    }
123
124        /// Creates a new ERN with the given root and default values for other components.
125        ///
126        /// This is a convenient way to create an ERN when you only care about the root component.
127        ///
128        /// # Arguments
129        ///
130        /// * `root` - The string value for the root component
131        ///
132        /// # Returns
133        ///
134        /// * `Ok(Ern)` - The created ERN with default values for domain, category, account, and parts
135        /// * `Err(ErnError)` - If the root value is invalid
136        ///
137        /// # Example
138        ///
139        /// ```
140        /// # use acton_ern::prelude::*;
141        /// # fn example() -> Result<(), ErnError> {
142        /// let ern = Ern::with_root("profile")?;
143        /// # Ok(())
144        /// # }
145        /// ```
146        pub fn with_root(root: impl Into<String>) -> Result<Self, ErnError> {
147            let root = EntityRoot::new(root.into())?;
148            Ok(Ern {
149                root,
150                ..Default::default()
151            })
152        }
153
154        /// Creates a new ERN based on an existing ERN but with a different root.
155        ///
156        /// This method preserves all other components (domain, category, account, parts)
157        /// but replaces the root with a new value.
158        ///
159        /// # Arguments
160        ///
161        /// * `new_root` - The string value for the new root component
162        ///
163        /// # Returns
164        ///
165        /// * `Ok(Ern)` - A new ERN with the updated root
166        /// * `Err(ErnError)` - If the new root value is invalid
167        ///
168        /// # Example
169        ///
170        /// ```
171        /// # use acton_ern::prelude::*;
172        /// # fn example() -> Result<(), ErnError> {
173        /// let ern1 = Ern::with_root("profile")?;
174        /// let ern2 = ern1.with_new_root("settings")?;
175        /// # Ok(())
176        /// # }
177        /// ```
178        pub fn with_new_root(&self, new_root: impl Into<String>) -> Result<Self, ErnError> {
179            let new_root = EntityRoot::new(new_root.into())?;
180            Ok(Ern {
181                domain: self.domain.clone(),
182                category: self.category.clone(),
183                account: self.account.clone(),
184                root: new_root,
185                parts: self.parts.clone(),
186            })
187        }
188
189        pub fn with_domain(domain: impl Into<String>) -> Result<Self, ErnError> {
190            let domain = Domain::new(domain)?;
191            Ok(Ern {
192                domain,
193                category: Category::default(),
194                account: Account::default(),
195                root: EntityRoot::default(),
196                parts: Parts::default(),
197            })
198        }
199
200        pub fn with_category(category: impl Into<String>) -> Result<Self, ErnError> {
201            let category = Category::new(category)?;
202            Ok(Ern {
203                domain: Domain::default(),
204                category,
205                account: Account::default(),
206                root: EntityRoot::default(),
207                parts: Parts::default(),
208            })
209        }
210
211        pub fn with_account(account: impl Into<String>) -> Result<Self, ErnError> {
212            let account = Account::new(account)?;
213            Ok(Ern {
214                domain: Domain::default(),
215                category: Category::default(),
216                account,
217                root: EntityRoot::default(),
218                parts: Parts::default(),
219            })
220        }
221
222        /// Adds a new part to the ERN's path.
223        ///
224        /// This method creates a new ERN with the same domain, category, account, and root,
225        /// but with an additional part appended to the path.
226        ///
227        /// # Arguments
228        ///
229        /// * `part` - The string value for the new part
230        ///
231        /// # Returns
232        ///
233        /// * `Ok(Ern)` - A new ERN with the added part
234        /// * `Err(ErnError)` - If the part value is invalid or adding it would exceed the maximum of 10 parts
235        ///
236        /// # Example
237        ///
238        /// ```
239        /// # use acton_ern::prelude::*;
240        /// # fn example() -> Result<(), ErnError> {
241        /// let ern1 = Ern::with_root("profile")?;
242        /// let ern2 = ern1.add_part("settings")?;
243        /// let ern3 = ern2.add_part("appearance")?;
244        /// # Ok(())
245        /// # }
246        /// ```
247        pub fn add_part(&self, part: impl Into<String>) -> Result<Self, ErnError> {
248            let new_part = Part::new(part)?;
249            let mut new_parts = self.parts.clone();
250            
251            // Check if adding another part would exceed the maximum
252            if new_parts.0.len() >= 10 {
253                return Err(ErnError::ParseFailure(
254                    "Parts",
255                    "cannot exceed maximum of 10 parts".to_string(),
256                ));
257            }
258            
259            new_parts.0.push(new_part);
260            Ok(Ern {
261                domain: self.domain.clone(),
262                category: self.category.clone(),
263                account: self.account.clone(),
264                root: self.root.clone(),
265                parts: new_parts,
266            })
267        }
268
269        pub fn with_parts(
270            &self,
271            parts: impl IntoIterator<Item = impl Into<String>>,
272        ) -> Result<Self, ErnError> {
273            let new_parts: Result<Vec<Part>, _> = parts.into_iter().map(Part::new).collect();
274            Ok(Ern {
275                domain: self.domain.clone(),
276                category: self.category.clone(),
277                account: self.account.clone(),
278                root: self.root.clone(),
279                parts: Parts(new_parts?),
280            })
281        }
282
283        /// Checks if this ERN is a child of another ERN.
284        ///
285        /// An ERN is considered a child of another ERN if:
286        /// 1. They have the same domain, category, account, and root
287        /// 2. The child's parts start with all of the parent's parts
288        /// 3. The child has more parts than the parent
289        ///
290        /// # Arguments
291        ///
292        /// * `other` - The potential parent ERN
293        ///
294        /// # Returns
295        ///
296        /// * `true` - If this ERN is a child of the other ERN
297        /// * `false` - Otherwise
298        ///
299        /// # Example
300        ///
301        /// ```
302        /// # use acton_ern::prelude::*;
303        /// # fn example() -> Result<(), ErnError> {
304        /// let parent = Ern::with_root("profile")?.add_part("settings")?;
305        /// let child = parent.add_part("appearance")?;
306        ///
307        /// assert!(child.is_child_of(&parent));
308        /// assert!(!parent.is_child_of(&child));
309        /// # Ok(())
310        /// # }
311        /// ```
312        pub fn is_child_of(&self, other: &Ern) -> bool {
313            self.domain == other.domain
314                && self.category == other.category
315                && self.account == other.account
316                && self.root == other.root
317                && other.parts.0.len() < self.parts.0.len()
318                && self.parts.0.starts_with(&other.parts.0)
319        }
320
321        /// Returns the parent ERN of this ERN, if it exists.
322        ///
323        /// The parent ERN has the same domain, category, account, and root,
324        /// but with one fewer part in the path. If this ERN has no parts,
325        /// it has no parent and this method returns `None`.
326        ///
327        /// # Returns
328        ///
329        /// * `Some(Ern)` - The parent ERN
330        /// * `None` - If this ERN has no parts (and thus no parent)
331        ///
332        /// # Example
333        ///
334        /// ```
335        /// # use acton_ern::prelude::*;
336        /// # fn example() -> Result<(), ErnError> {
337        /// let ern1 = Ern::with_root("profile")?;
338        /// let ern2 = ern1.add_part("settings")?;
339        /// let ern3 = ern2.add_part("appearance")?;
340        ///
341        /// assert_eq!(ern3.parent().unwrap().to_string(), ern2.to_string());
342        /// assert_eq!(ern2.parent().unwrap().to_string(), ern1.to_string());
343        /// assert!(ern1.parent().is_none());
344        /// # Ok(())
345        /// # }
346        /// ```
347        pub fn parent(&self) -> Option<Self> {
348            if self.parts.0.is_empty() {
349                None
350            } else {
351                Some(Ern {
352                    domain: self.domain.clone(),
353                    category: self.category.clone(),
354                    account: self.account.clone(),
355                    root: self.root.clone(),
356                    parts: Parts(self.parts.0[..self.parts.0.len() - 1].to_vec()),
357                })
358            }
359        }
360}
361
362impl Default for Ern {
363    /// Provides a default ERN using the default values of all its components.
364    ///
365    /// This is primarily used internally and for testing. For creating ERNs in
366    /// application code, prefer using `ErnBuilder` or the `with_root` method.
367    fn default() -> Self {
368        Ern {
369            domain: Domain::default(),
370            category: Category::default(),
371            account: Account::default(),
372            root: EntityRoot::default(),
373            parts: Parts::new(Vec::default()),
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use std::str::FromStr;
381    use std::thread::sleep;
382    use std::time::Duration;
383
384    use crate::Part;
385
386    use super::*;
387
388    #[test]
389    fn test_ern_timestamp_ordering() {
390        let ern1: Ern = Ern::with_root("root_a").unwrap();
391        sleep(Duration::from_millis(10));
392        let ern2: Ern = Ern::with_root("root_b").unwrap();
393        sleep(Duration::from_millis(10));
394        let ern3: Ern = Ern::with_root("root_c").unwrap();
395
396        // Test ascending order
397        assert!(ern1 < ern2);
398        assert!(ern2 < ern3);
399        assert!(ern1 < ern3);
400
401        // Test descending order
402        assert!(ern3 > ern2);
403        assert!(ern2 > ern1);
404        assert!(ern3 > ern1);
405
406        // Test equality
407        let ern1_clone = ern1.clone();
408        assert_eq!(ern1, ern1_clone);
409
410        // Test sorting
411        let mut erns = vec![ern3.clone(), ern1.clone(), ern2.clone()];
412        erns.sort();
413        assert_eq!(erns, vec![ern1, ern2, ern3]);
414    }
415
416    #[test]
417    fn test_ern_unixtime_ordering() {
418        let ern1: Ern = Ern::with_root("root_a").unwrap();
419        sleep(Duration::from_millis(10));
420        let ern2: Ern = Ern::with_root("root_b").unwrap();
421        sleep(Duration::from_millis(10));
422        let ern3: Ern = Ern::with_root("root_c").unwrap();
423
424        // Test ascending order
425        assert!(ern1 < ern2);
426        assert!(ern2 < ern3);
427        assert!(ern1 < ern3);
428
429        // Test descending order
430        assert!(ern3 > ern2);
431        assert!(ern2 > ern1);
432        assert!(ern3 > ern1);
433
434        // Test equality
435        let ern1_clone = ern1.clone();
436        assert_eq!(ern1, ern1_clone);
437
438        // Test sorting
439        let mut erns = vec![ern3.clone(), ern1.clone(), ern2.clone()];
440        erns.sort();
441        assert_eq!(erns, vec![ern1, ern2, ern3]);
442    }
443
444    #[test]
445    fn test_ern_timestamp_unixtime_consistency() {
446        let ern_timestamp1: Ern = Ern::with_root("root_a").unwrap();
447        let ern_unixtime1: Ern = Ern::with_root("root_a").unwrap();
448
449        sleep(Duration::from_millis(10));
450
451        let ern_timestamp2: Ern = Ern::with_root("root_b").unwrap();
452        let ern_unixtime2: Ern = Ern::with_root("root_b").unwrap();
453
454        // Ensure that the ordering is consistent between Timestamp and UnixTime
455        assert_eq!(
456            ern_timestamp1 < ern_timestamp2,
457            ern_unixtime1 < ern_unixtime2
458        );
459    }
460    #[test]
461    fn test_ern_with_root() {
462        let ern: Ern = Ern::with_root("custom_root").unwrap();
463        assert!(ern.root.as_str().starts_with("custom_root"));
464        assert_eq!(ern.domain, Domain::default());
465        assert_eq!(ern.category, Category::default());
466        assert_eq!(ern.account, Account::default());
467        assert_eq!(ern.parts, Parts::default());
468    }
469
470    #[test]
471    fn test_ern_with_new_root() {
472        let original_ern: Ern = Ern::default();
473        let new_ern: Ern = original_ern.with_new_root("new_root").unwrap();
474        assert!(new_ern.root.as_str().starts_with("new_root"));
475        assert_eq!(new_ern.domain, original_ern.domain);
476        assert_eq!(new_ern.category, original_ern.category);
477        assert_eq!(new_ern.account, original_ern.account);
478        assert_eq!(new_ern.parts, original_ern.parts);
479    }
480
481    #[test]
482    fn test_add_erns() -> anyhow::Result<()> {
483        let parent_root = EntityRoot::from_str("root_a")?;
484        let parent: Ern = Ern::new(
485            Domain::from_str("acton-internal").unwrap(),
486            Category::from_str("hr").unwrap(),
487            Account::from_str("company123").unwrap(),
488            parent_root.clone(),
489            Parts(vec![
490                Part::from_str("department_a").unwrap(),
491                Part::from_str("team1").unwrap(),
492            ]),
493        );
494
495        let child: Ern = Ern::new(
496            Domain::from_str("acton-internal").unwrap(),
497            Category::from_str("hr").unwrap(),
498            Account::from_str("company123").unwrap(),
499            EntityRoot::from_str("root_b").unwrap(),
500            Parts(vec![Part::from_str("role_x").unwrap()]),
501        );
502
503        let combined: Ern = parent + child;
504
505        assert_eq!(combined.domain, Domain::from_str("acton-internal").unwrap());
506        assert_eq!(combined.category, Category::from_str("hr").unwrap());
507        assert_eq!(combined.account, Account::from_str("company123").unwrap());
508        assert_eq!(combined.root, parent_root);
509        assert_eq!(
510            combined.parts,
511            Parts(vec![
512                Part::from_str("department_a").unwrap(),
513                Part::from_str("team1").unwrap(),
514                Part::from_str("role_x").unwrap(),
515            ])
516        );
517        Ok(())
518    }
519
520    #[test]
521    fn test_add_erns_empty_child() {
522        let parent: Ern = Ern::new(
523            Domain::from_str("acton-internal").unwrap(),
524            Category::from_str("hr").unwrap(),
525            Account::from_str("company123").unwrap(),
526            EntityRoot::from_str("rootp").unwrap(),
527            Parts(vec![Part::from_str("department_a").unwrap()]),
528        );
529
530        let child: Ern = Ern::new(
531            Domain::from_str("acton-internal").unwrap(),
532            Category::from_str("hr").unwrap(),
533            Account::from_str("company123").unwrap(),
534            EntityRoot::from_str("rootc").unwrap(),
535            Parts(vec![]),
536        );
537
538        let combined = parent + child;
539
540        assert_eq!(
541            combined.parts,
542            Parts(vec![Part::from_str("department_a").unwrap()])
543        );
544    }
545
546    #[test]
547    fn test_add_erns_empty_parent() {
548        let parent: Ern = Ern::new(
549            Domain::from_str("acton-internal").unwrap(),
550            Category::from_str("hr").unwrap(),
551            Account::from_str("company123").unwrap(),
552            EntityRoot::from_str("rootp").unwrap(),
553            Parts(vec![]),
554        );
555        let child: Ern = Ern::new(
556            Domain::from_str("acton-internal").unwrap(),
557            Category::from_str("hr").unwrap(),
558            Account::from_str("company123").unwrap(),
559            EntityRoot::from_str("rootc").unwrap(),
560            Parts(vec![Part::from_str("role_x").unwrap()]),
561        );
562        let combined = parent + child;
563        assert_eq!(
564            combined.parts,
565            Parts(vec![Part::from_str("role_x").unwrap()])
566        );
567    }
568
569    #[test]
570    fn test_add_erns_display() {
571        let parent: Ern = Ern::new(
572            Domain::from_str("acton-internal").unwrap(),
573            Category::from_str("hr").unwrap(),
574            Account::from_str("company123").unwrap(),
575            EntityRoot::from_str("rootp").unwrap(),
576            Parts(vec![Part::from_str("department_a").unwrap()]),
577        );
578
579        let child: Ern = Ern::new(
580            Domain::from_str("acton-internal").unwrap(),
581            Category::from_str("hr").unwrap(),
582            Account::from_str("company123").unwrap(),
583            EntityRoot::from_str("rootc").unwrap(),
584            Parts(vec![Part::from_str("team1").unwrap()]),
585        );
586
587        let combined = parent + child;
588
589        assert!(combined
590            .to_string()
591            .starts_with("ern:acton-internal:hr:company123:rootp"));
592    }
593    #[test]
594    fn test_ern_custom() -> anyhow::Result<()> {
595        let ern: Ern = Ern::new(
596            Domain::new("custom")?,
597            Category::new("service")?,
598            Account::new("account123")?,
599            EntityRoot::new("root".to_string())?,
600            Parts::new(vec![Part::new("resource")?]),
601        );
602        assert!(ern
603            .to_string()
604            .starts_with("ern:custom:service:account123:root"));
605        Ok(())
606    }
607
608    #[test]
609    fn test_ern_append_invalid_part() -> anyhow::Result<()> {
610        let invalid_part = Part::new(":invalid");
611
612        assert!(invalid_part.is_err());
613        Ok(())
614    }
615}