use std::cmp::Ordering;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Add;
use std::sync::Arc;
use crate::errors::ErnError;
use crate::{Account, Category, Domain, EntityRoot, ErnComponent, Part, Parts};
#[derive(Debug, PartialEq, Eq, Hash)]
struct ErnInner {
domain: Domain,
category: Category,
account: Account,
root: EntityRoot,
parts: Parts,
}
#[derive(Debug, Clone)]
pub struct Ern {
inner: Arc<ErnInner>,
}
impl Ern {
pub fn new(
domain: Domain,
category: Category,
account: Account,
root: EntityRoot,
parts: Parts,
) -> Self {
Ern {
inner: Arc::new(ErnInner {
domain,
category,
account,
root,
parts,
}),
}
}
fn from_inner(inner: ErnInner) -> Self {
Ern {
inner: Arc::new(inner),
}
}
pub fn domain(&self) -> &Domain {
&self.inner.domain
}
pub fn category(&self) -> &Category {
&self.inner.category
}
pub fn account(&self) -> &Account {
&self.inner.account
}
pub fn root(&self) -> &EntityRoot {
&self.inner.root
}
pub fn parts(&self) -> &Parts {
&self.inner.parts
}
pub fn with_root(root: impl Into<String>) -> Result<Self, ErnError> {
let root = EntityRoot::new(root.into())?;
Ok(Ern::from_inner(ErnInner {
root,
domain: Domain::default(),
category: Category::default(),
account: Account::default(),
parts: Parts::new(Vec::default()),
}))
}
pub fn with_new_root(&self, new_root: impl Into<String>) -> Result<Self, ErnError> {
let new_root = EntityRoot::new(new_root.into())?;
Ok(Ern::from_inner(ErnInner {
domain: self.inner.domain.clone(),
category: self.inner.category.clone(),
account: self.inner.account.clone(),
root: new_root,
parts: self.inner.parts.clone(),
}))
}
pub fn with_domain(domain: impl Into<String>) -> Result<Self, ErnError> {
let domain = Domain::new(domain)?;
Ok(Ern::from_inner(ErnInner {
domain,
category: Category::default(),
account: Account::default(),
root: EntityRoot::default(),
parts: Parts::default(),
}))
}
pub fn with_category(category: impl Into<String>) -> Result<Self, ErnError> {
let category = Category::new(category)?;
Ok(Ern::from_inner(ErnInner {
domain: Domain::default(),
category,
account: Account::default(),
root: EntityRoot::default(),
parts: Parts::default(),
}))
}
pub fn with_account(account: impl Into<String>) -> Result<Self, ErnError> {
let account = Account::new(account)?;
Ok(Ern::from_inner(ErnInner {
domain: Domain::default(),
category: Category::default(),
account,
root: EntityRoot::default(),
parts: Parts::default(),
}))
}
pub fn add_part(&self, part: impl Into<String>) -> Result<Self, ErnError> {
let new_part = Part::new(part)?;
let mut new_parts = self.inner.parts.clone();
if new_parts.0.len() >= 10 {
return Err(ErnError::ParseFailure(
"Parts",
"cannot exceed maximum of 10 parts".to_string(),
));
}
new_parts.0.push(new_part);
Ok(Ern::from_inner(ErnInner {
domain: self.inner.domain.clone(),
category: self.inner.category.clone(),
account: self.inner.account.clone(),
root: self.inner.root.clone(),
parts: new_parts,
}))
}
pub fn with_parts(
&self,
parts: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self, ErnError> {
let new_parts: Result<Vec<Part>, _> = parts.into_iter().map(Part::new).collect();
Ok(Ern::from_inner(ErnInner {
domain: self.inner.domain.clone(),
category: self.inner.category.clone(),
account: self.inner.account.clone(),
root: self.inner.root.clone(),
parts: Parts(new_parts?),
}))
}
pub fn is_child_of(&self, other: &Ern) -> bool {
self.inner.domain == other.inner.domain
&& self.inner.category == other.inner.category
&& self.inner.account == other.inner.account
&& self.inner.root == other.inner.root
&& other.inner.parts.0.len() < self.inner.parts.0.len()
&& self.inner.parts.0.starts_with(&other.inner.parts.0)
}
pub fn parent(&self) -> Option<Self> {
if self.inner.parts.0.is_empty() {
None
} else {
Some(Ern::from_inner(ErnInner {
domain: self.inner.domain.clone(),
category: self.inner.category.clone(),
account: self.inner.account.clone(),
root: self.inner.root.clone(),
parts: Parts(self.inner.parts.0[..self.inner.parts.0.len() - 1].to_vec()),
}))
}
}
}
impl PartialEq for Ern {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner) || self.inner == other.inner
}
}
impl Eq for Ern {}
impl Hash for Ern {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl Ord for Ern {
fn cmp(&self, other: &Self) -> Ordering {
self.inner.root.name().cmp(other.inner.root.name())
}
}
impl PartialOrd for Ern {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Display for Ern {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut display = format!(
"{}{}:{}:{}:{}",
Domain::prefix(),
self.inner.domain,
self.inner.category,
self.inner.account,
self.inner.root
);
if !self.inner.parts.0.is_empty() {
display = format!("{}/{}", display, self.inner.parts);
}
write!(f, "{}", display)
}
}
impl Add for Ern {
type Output = Ern;
fn add(self, rhs: Self) -> Self::Output {
let mut new_parts = self.inner.parts.0.clone();
new_parts.extend(rhs.inner.parts.0.iter().cloned());
Ern::from_inner(ErnInner {
domain: self.inner.domain.clone(),
category: self.inner.category.clone(),
account: self.inner.account.clone(),
root: self.inner.root.clone(),
parts: Parts(new_parts),
})
}
}
impl Default for Ern {
fn default() -> Self {
Ern::new(
Domain::default(),
Category::default(),
Account::default(),
EntityRoot::default(),
Parts::new(Vec::default()),
)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Ern {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("Ern", 5)?;
state.serialize_field("domain", &self.inner.domain)?;
state.serialize_field("category", &self.inner.category)?;
state.serialize_field("account", &self.inner.account)?;
state.serialize_field("root", &self.inner.root)?;
state.serialize_field("parts", &self.inner.parts)?;
state.end()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Ern {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct ErnFields {
domain: Domain,
category: Category,
account: Account,
root: EntityRoot,
parts: Parts,
}
let fields = ErnFields::deserialize(deserializer)?;
Ok(Ern::new(
fields.domain,
fields.category,
fields.account,
fields.root,
fields.parts,
))
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
use crate::Part;
use super::*;
#[test]
fn test_ern_timestamp_ordering() {
let ern1: Ern = Ern::with_root("root_a").unwrap();
sleep(Duration::from_millis(10));
let ern2: Ern = Ern::with_root("root_b").unwrap();
sleep(Duration::from_millis(10));
let ern3: Ern = Ern::with_root("root_c").unwrap();
assert!(ern1 < ern2);
assert!(ern2 < ern3);
assert!(ern1 < ern3);
assert!(ern3 > ern2);
assert!(ern2 > ern1);
assert!(ern3 > ern1);
let ern1_clone = ern1.clone();
assert_eq!(ern1, ern1_clone);
let mut erns = vec![ern3.clone(), ern1.clone(), ern2.clone()];
erns.sort();
assert_eq!(erns, vec![ern1, ern2, ern3]);
}
#[test]
fn test_ern_unixtime_ordering() {
let ern1: Ern = Ern::with_root("root_a").unwrap();
sleep(Duration::from_millis(10));
let ern2: Ern = Ern::with_root("root_b").unwrap();
sleep(Duration::from_millis(10));
let ern3: Ern = Ern::with_root("root_c").unwrap();
assert!(ern1 < ern2);
assert!(ern2 < ern3);
assert!(ern1 < ern3);
assert!(ern3 > ern2);
assert!(ern2 > ern1);
assert!(ern3 > ern1);
let ern1_clone = ern1.clone();
assert_eq!(ern1, ern1_clone);
let mut erns = vec![ern3.clone(), ern1.clone(), ern2.clone()];
erns.sort();
assert_eq!(erns, vec![ern1, ern2, ern3]);
}
#[test]
fn test_ern_timestamp_unixtime_consistency() {
let ern_timestamp1: Ern = Ern::with_root("root_a").unwrap();
let ern_unixtime1: Ern = Ern::with_root("root_a").unwrap();
sleep(Duration::from_millis(10));
let ern_timestamp2: Ern = Ern::with_root("root_b").unwrap();
let ern_unixtime2: Ern = Ern::with_root("root_b").unwrap();
assert_eq!(
ern_timestamp1 < ern_timestamp2,
ern_unixtime1 < ern_unixtime2
);
}
#[test]
fn test_ern_with_root() {
let ern: Ern = Ern::with_root("custom_root").unwrap();
assert!(ern.root().as_str().starts_with("custom_root"));
assert_eq!(*ern.domain(), Domain::default());
assert_eq!(*ern.category(), Category::default());
assert_eq!(*ern.account(), Account::default());
assert_eq!(*ern.parts(), Parts::default());
}
#[test]
fn test_ern_with_new_root() {
let original_ern: Ern = Ern::default();
let new_ern: Ern = original_ern.with_new_root("new_root").unwrap();
assert!(new_ern.root().as_str().starts_with("new_root"));
assert_eq!(new_ern.domain(), original_ern.domain());
assert_eq!(new_ern.category(), original_ern.category());
assert_eq!(new_ern.account(), original_ern.account());
assert_eq!(new_ern.parts(), original_ern.parts());
}
#[test]
fn test_add_erns() -> anyhow::Result<()> {
let parent_root = EntityRoot::from_str("root_a")?;
let parent: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
parent_root.clone(),
Parts(vec![
Part::from_str("department_a").unwrap(),
Part::from_str("team1").unwrap(),
]),
);
let child: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
EntityRoot::from_str("root_b").unwrap(),
Parts(vec![Part::from_str("role_x").unwrap()]),
);
let combined: Ern = parent + child;
assert_eq!(*combined.domain(), Domain::from_str("acton-internal").unwrap());
assert_eq!(*combined.category(), Category::from_str("hr").unwrap());
assert_eq!(*combined.account(), Account::from_str("company123").unwrap());
assert_eq!(*combined.root(), parent_root);
assert_eq!(
*combined.parts(),
Parts(vec![
Part::from_str("department_a").unwrap(),
Part::from_str("team1").unwrap(),
Part::from_str("role_x").unwrap(),
])
);
Ok(())
}
#[test]
fn test_add_erns_empty_child() {
let parent: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
EntityRoot::from_str("rootp").unwrap(),
Parts(vec![Part::from_str("department_a").unwrap()]),
);
let child: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
EntityRoot::from_str("rootc").unwrap(),
Parts(vec![]),
);
let combined = parent + child;
assert_eq!(
*combined.parts(),
Parts(vec![Part::from_str("department_a").unwrap()])
);
}
#[test]
fn test_add_erns_empty_parent() {
let parent: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
EntityRoot::from_str("rootp").unwrap(),
Parts(vec![]),
);
let child: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
EntityRoot::from_str("rootc").unwrap(),
Parts(vec![Part::from_str("role_x").unwrap()]),
);
let combined = parent + child;
assert_eq!(
*combined.parts(),
Parts(vec![Part::from_str("role_x").unwrap()])
);
}
#[test]
fn test_add_erns_display() {
let parent: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
EntityRoot::from_str("rootp").unwrap(),
Parts(vec![Part::from_str("department_a").unwrap()]),
);
let child: Ern = Ern::new(
Domain::from_str("acton-internal").unwrap(),
Category::from_str("hr").unwrap(),
Account::from_str("company123").unwrap(),
EntityRoot::from_str("rootc").unwrap(),
Parts(vec![Part::from_str("team1").unwrap()]),
);
let combined = parent + child;
assert!(
combined
.to_string()
.starts_with("ern:acton-internal:hr:company123:rootp")
);
}
#[test]
fn test_ern_custom() -> anyhow::Result<()> {
let ern: Ern = Ern::new(
Domain::new("custom")?,
Category::new("service")?,
Account::new("account123")?,
EntityRoot::new("root".to_string())?,
Parts::new(vec![Part::new("resource")?]),
);
assert!(
ern.to_string()
.starts_with("ern:custom:service:account123:root")
);
Ok(())
}
#[test]
fn test_ern_append_invalid_part() -> anyhow::Result<()> {
let invalid_part = Part::new(":invalid");
assert!(invalid_part.is_err());
Ok(())
}
}