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#[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 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 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 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 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 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 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 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 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 assert!(ern1 < ern2);
398 assert!(ern2 < ern3);
399 assert!(ern1 < ern3);
400
401 assert!(ern3 > ern2);
403 assert!(ern2 > ern1);
404 assert!(ern3 > ern1);
405
406 let ern1_clone = ern1.clone();
408 assert_eq!(ern1, ern1_clone);
409
410 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 assert!(ern1 < ern2);
426 assert!(ern2 < ern3);
427 assert!(ern1 < ern3);
428
429 assert!(ern3 > ern2);
431 assert!(ern2 > ern1);
432 assert!(ern3 > ern1);
433
434 let ern1_clone = ern1.clone();
436 assert_eq!(ern1, ern1_clone);
437
438 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 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}