actr_protocol/
actr_ext.rs1use crate::{ActrId, ActrType, Realm, name::Name};
14use std::str::FromStr;
15use thiserror::Error;
16
17#[derive(Error, Debug, PartialEq, Eq)]
22pub enum ActrIdError {
23 #[error(
24 "Invalid Actor ID format: '{0}'. Expected: <serial_hex>@<realm_id>/<manufacturer>:<name>:<version>"
25 )]
26 InvalidFormat(String),
27
28 #[error("Invalid component in actor identity: {0}")]
29 InvalidComponent(String),
30
31 #[error("Invalid actor type format: '{0}'. Expected: <manufacturer>:<name>:<version>")]
32 InvalidTypeFormat(String),
33}
34
35impl ActrType {
36 pub fn to_string_repr(&self) -> String {
40 debug_assert!(
41 !self.version.is_empty(),
42 "ActrType.version must be non-empty"
43 );
44
45 format!("{}:{}:{}", self.manufacturer, self.name, self.version)
46 }
47
48 pub fn from_string_repr(s: &str) -> Result<Self, ActrIdError> {
56 let parts: Vec<&str> = s.splitn(4, ':').collect();
58 let (manufacturer, name, version) = match parts.as_slice() {
59 [_, _] => {
60 return Err(ActrIdError::InvalidTypeFormat(format!(
61 "{s} (version is required, expected <manufacturer>:<name>:<version>)"
62 )));
63 }
64 [m, n, v] => (*m, *n, *v),
65 _ => return Err(ActrIdError::InvalidTypeFormat(s.to_string())),
66 };
67
68 Name::new(manufacturer.to_string())
69 .map_err(|e| ActrIdError::InvalidComponent(format!("Invalid manufacturer: {e}")))?;
70 Name::new(name.to_string())
71 .map_err(|e| ActrIdError::InvalidComponent(format!("Invalid type name: {e}")))?;
72 if version.is_empty() {
73 return Err(ActrIdError::InvalidComponent(
74 "Invalid version: version cannot be empty".to_string(),
75 ));
76 }
77
78 Ok(ActrType {
79 manufacturer: manufacturer.to_string(),
80 name: name.to_string(),
81 version: version.to_string(),
82 })
83 }
84}
85
86impl std::fmt::Display for ActrType {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.to_string_repr())
89 }
90}
91
92impl ActrId {
93 pub fn to_string_repr(&self) -> String {
98 format!(
99 "{:x}@{}/{}",
100 self.serial_number,
101 self.realm.realm_id,
102 self.r#type.to_string_repr()
103 )
104 }
105
106 pub fn from_string_repr(s: &str) -> Result<Self, ActrIdError> {
108 let (serial_part, rest) = s
110 .split_once('@')
111 .ok_or_else(|| ActrIdError::InvalidFormat("Missing '@' separator".to_string()))?;
112
113 let serial_number = u64::from_str_radix(serial_part, 16).map_err(|_| {
114 ActrIdError::InvalidComponent(format!("Invalid serial number hex: {serial_part}"))
115 })?;
116
117 let (realm_part, type_part) = rest
118 .split_once('/')
119 .ok_or_else(|| ActrIdError::InvalidFormat("Missing '/' separator".to_string()))?;
120
121 let realm_id = u32::from_str(realm_part).map_err(|_| {
122 ActrIdError::InvalidComponent(format!("Invalid realm ID: {realm_part}"))
123 })?;
124
125 let actr_type = ActrType::from_string_repr(type_part)?;
126
127 Ok(ActrId {
128 realm: Realm { realm_id },
129 serial_number,
130 r#type: actr_type,
131 })
132 }
133}
134
135impl std::fmt::Display for ActrId {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 write!(f, "{}", self.to_string_repr())
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_actor_id_roundtrip_with_version() {
147 let original = ActrId {
148 realm: Realm { realm_id: 101 },
149 serial_number: 0x1a2b3c,
150 r#type: ActrType {
151 manufacturer: "acme".to_string(),
152 name: "echo-service".to_string(),
153 version: "1.0.0".to_string(),
154 },
155 };
156
157 let s = original.to_string_repr();
158 assert_eq!(s, "1a2b3c@101/acme:echo-service:1.0.0");
159
160 let parsed = ActrId::from_string_repr(&s).unwrap();
161 assert_eq!(parsed.realm.realm_id, original.realm.realm_id);
162 assert_eq!(parsed.serial_number, original.serial_number);
163 assert_eq!(parsed.r#type.manufacturer, original.r#type.manufacturer);
164 assert_eq!(parsed.r#type.name, original.r#type.name);
165 assert_eq!(parsed.r#type.version, original.r#type.version);
166 }
167
168 #[test]
169 fn test_actor_id_roundtrip_without_version_errors() {
170 let result = ActrId::from_string_repr("1a2b3c@101/acme:echo-service");
172 assert!(
173 matches!(result, Err(ActrIdError::InvalidTypeFormat(_))),
174 "Expected InvalidTypeFormat error, got: {:?}",
175 result
176 );
177 }
178
179 #[test]
180 fn test_invalid_actor_id_format() {
181 assert!(matches!(
182 ActrId::from_string_repr("invalid-string"),
183 Err(ActrIdError::InvalidFormat(_))
184 ));
185 assert!(matches!(
187 ActrId::from_string_repr("123@101"),
188 Err(ActrIdError::InvalidFormat(_))
189 ));
190 assert!(matches!(
192 ActrId::from_string_repr("xyz@101/acme:echo"),
193 Err(ActrIdError::InvalidComponent(_))
194 ));
195 }
196
197 #[test]
198 fn test_actr_type_roundtrip_with_version() {
199 let s = "acme:echo:1.2.3";
200 let ty = ActrType::from_string_repr(s).unwrap();
201 assert_eq!(ty.manufacturer, "acme");
202 assert_eq!(ty.name, "echo");
203 assert_eq!(ty.version.as_str(), "1.2.3");
204 assert_eq!(ty.to_string_repr(), s);
205 }
206
207 #[test]
208 fn test_actr_type_without_version_is_error() {
209 let s = "acme:echo-service";
211 let result = ActrType::from_string_repr(s);
212 assert!(
213 matches!(result, Err(ActrIdError::InvalidTypeFormat(_))),
214 "Expected InvalidTypeFormat, got: {:?}",
215 result
216 );
217 }
218
219 #[test]
220 fn test_actr_type_invalid_format() {
221 assert!(matches!(
223 ActrType::from_string_repr("acme-echo"),
224 Err(ActrIdError::InvalidTypeFormat(_))
225 ));
226 assert!(matches!(
228 ActrType::from_string_repr("a:b:c:d"),
229 Err(ActrIdError::InvalidTypeFormat(_))
230 ));
231 assert!(matches!(
233 ActrType::from_string_repr("1acme:echo:1.0.0"),
234 Err(ActrIdError::InvalidComponent(_))
235 ));
236 assert!(matches!(
238 ActrType::from_string_repr("acme:echo!:1.0.0"),
239 Err(ActrIdError::InvalidComponent(_))
240 ));
241 assert!(matches!(
242 ActrType::from_string_repr("acme:echo:"),
243 Err(ActrIdError::InvalidComponent(_))
244 ));
245 }
246}