use crate::{ActrId, ActrType, Realm, name::Name};
use std::str::FromStr;
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum ActrIdError {
#[error(
"Invalid Actor ID format: '{0}'. Expected: <serial_hex>@<realm_id>/<manufacturer>:<name>:<version>"
)]
InvalidFormat(String),
#[error("Invalid component in actor identity: {0}")]
InvalidComponent(String),
#[error("Invalid actor type format: '{0}'. Expected: <manufacturer>:<name>:<version>")]
InvalidTypeFormat(String),
}
impl ActrType {
pub fn to_string_repr(&self) -> String {
debug_assert!(
!self.version.is_empty(),
"ActrType.version must be non-empty"
);
format!("{}:{}:{}", self.manufacturer, self.name, self.version)
}
pub fn from_string_repr(s: &str) -> Result<Self, ActrIdError> {
let parts: Vec<&str> = s.splitn(4, ':').collect();
let (manufacturer, name, version) = match parts.as_slice() {
[_, _] => {
return Err(ActrIdError::InvalidTypeFormat(format!(
"{s} (version is required, expected <manufacturer>:<name>:<version>)"
)));
}
[m, n, v] => (*m, *n, *v),
_ => return Err(ActrIdError::InvalidTypeFormat(s.to_string())),
};
Name::new(manufacturer.to_string())
.map_err(|e| ActrIdError::InvalidComponent(format!("Invalid manufacturer: {e}")))?;
Name::new(name.to_string())
.map_err(|e| ActrIdError::InvalidComponent(format!("Invalid type name: {e}")))?;
if version.is_empty() {
return Err(ActrIdError::InvalidComponent(
"Invalid version: version cannot be empty".to_string(),
));
}
Ok(ActrType {
manufacturer: manufacturer.to_string(),
name: name.to_string(),
version: version.to_string(),
})
}
}
impl std::fmt::Display for ActrType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_string_repr())
}
}
impl ActrId {
pub fn to_string_repr(&self) -> String {
format!(
"{:x}@{}/{}",
self.serial_number,
self.realm.realm_id,
self.r#type.to_string_repr()
)
}
pub fn from_string_repr(s: &str) -> Result<Self, ActrIdError> {
let (serial_part, rest) = s
.split_once('@')
.ok_or_else(|| ActrIdError::InvalidFormat("Missing '@' separator".to_string()))?;
let serial_number = u64::from_str_radix(serial_part, 16).map_err(|_| {
ActrIdError::InvalidComponent(format!("Invalid serial number hex: {serial_part}"))
})?;
let (realm_part, type_part) = rest
.split_once('/')
.ok_or_else(|| ActrIdError::InvalidFormat("Missing '/' separator".to_string()))?;
let realm_id = u32::from_str(realm_part).map_err(|_| {
ActrIdError::InvalidComponent(format!("Invalid realm ID: {realm_part}"))
})?;
let actr_type = ActrType::from_string_repr(type_part)?;
Ok(ActrId {
realm: Realm { realm_id },
serial_number,
r#type: actr_type,
})
}
}
impl std::fmt::Display for ActrId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_string_repr())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_actor_id_roundtrip_with_version() {
let original = ActrId {
realm: Realm { realm_id: 101 },
serial_number: 0x1a2b3c,
r#type: ActrType {
manufacturer: "acme".to_string(),
name: "echo-service".to_string(),
version: "1.0.0".to_string(),
},
};
let s = original.to_string_repr();
assert_eq!(s, "1a2b3c@101/acme:echo-service:1.0.0");
let parsed = ActrId::from_string_repr(&s).unwrap();
assert_eq!(parsed.realm.realm_id, original.realm.realm_id);
assert_eq!(parsed.serial_number, original.serial_number);
assert_eq!(parsed.r#type.manufacturer, original.r#type.manufacturer);
assert_eq!(parsed.r#type.name, original.r#type.name);
assert_eq!(parsed.r#type.version, original.r#type.version);
}
#[test]
fn test_actor_id_roundtrip_without_version_errors() {
let result = ActrId::from_string_repr("1a2b3c@101/acme:echo-service");
assert!(
matches!(result, Err(ActrIdError::InvalidTypeFormat(_))),
"Expected InvalidTypeFormat error, got: {:?}",
result
);
}
#[test]
fn test_invalid_actor_id_format() {
assert!(matches!(
ActrId::from_string_repr("invalid-string"),
Err(ActrIdError::InvalidFormat(_))
));
assert!(matches!(
ActrId::from_string_repr("123@101"),
Err(ActrIdError::InvalidFormat(_))
));
assert!(matches!(
ActrId::from_string_repr("xyz@101/acme:echo"),
Err(ActrIdError::InvalidComponent(_))
));
}
#[test]
fn test_actr_type_roundtrip_with_version() {
let s = "acme:echo:1.2.3";
let ty = ActrType::from_string_repr(s).unwrap();
assert_eq!(ty.manufacturer, "acme");
assert_eq!(ty.name, "echo");
assert_eq!(ty.version.as_str(), "1.2.3");
assert_eq!(ty.to_string_repr(), s);
}
#[test]
fn test_actr_type_without_version_is_error() {
let s = "acme:echo-service";
let result = ActrType::from_string_repr(s);
assert!(
matches!(result, Err(ActrIdError::InvalidTypeFormat(_))),
"Expected InvalidTypeFormat, got: {:?}",
result
);
}
#[test]
fn test_actr_type_invalid_format() {
assert!(matches!(
ActrType::from_string_repr("acme-echo"),
Err(ActrIdError::InvalidTypeFormat(_))
));
assert!(matches!(
ActrType::from_string_repr("a:b:c:d"),
Err(ActrIdError::InvalidTypeFormat(_))
));
assert!(matches!(
ActrType::from_string_repr("1acme:echo:1.0.0"),
Err(ActrIdError::InvalidComponent(_))
));
assert!(matches!(
ActrType::from_string_repr("acme:echo!:1.0.0"),
Err(ActrIdError::InvalidComponent(_))
));
assert!(matches!(
ActrType::from_string_repr("acme:echo:"),
Err(ActrIdError::InvalidComponent(_))
));
}
}