1use serde::{Deserialize, Serialize};
9use std::fmt::{Display, Formatter};
10use std::str::FromStr;
11use thiserror::Error;
12
13#[derive(Error, Debug)]
15pub enum ActrUriError {
16 #[error("Invalid URI scheme, expected 'actr' but got '{0}'")]
17 InvalidScheme(String),
18
19 #[error("Missing actor authority in URI")]
20 MissingAuthority,
21
22 #[error("Invalid actor authority format, expected: <realm>:<manufacturer>+<name>@<version>")]
23 InvalidAuthorityFormat(String),
24
25 #[error("Missing version suffix '@<version>' in URI")]
26 MissingVersion,
27
28 #[error("Invalid realm ID: {0}")]
29 InvalidRealmId(String),
30
31 #[error("URI parse error: {0}")]
32 ParseError(String),
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct ActrUri {
39 pub realm: u32,
41 pub manufacturer: String,
43 pub name: String,
45 pub version: String,
47}
48
49impl ActrUri {
50 pub fn new(realm: u32, manufacturer: String, name: String, version: String) -> Self {
52 Self {
53 realm,
54 manufacturer,
55 name,
56 version,
57 }
58 }
59
60 pub fn scheme(&self) -> &'static str {
62 "actr"
63 }
64
65 pub fn actor_type(&self) -> String {
67 format!("{}+{}", self.manufacturer, self.name)
68 }
69}
70
71impl Display for ActrUri {
72 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73 write!(
74 f,
75 "actr://{}:{}+{}@{}",
76 self.realm, self.manufacturer, self.name, self.version
77 )
78 }
79}
80
81impl FromStr for ActrUri {
82 type Err = ActrUriError;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 if !s.starts_with("actr://") {
86 return Err(ActrUriError::InvalidScheme(
87 s.split(':').next().unwrap_or("").to_string(),
88 ));
89 }
90
91 let without_scheme = &s[7..];
92
93 if without_scheme.is_empty() {
95 return Err(ActrUriError::MissingAuthority);
96 }
97
98 let (authority, version) = without_scheme
100 .rsplit_once('@')
101 .ok_or(ActrUriError::MissingVersion)?;
102
103 let version = version.to_string();
104
105 let (realm_str, type_part) = authority
107 .split_once(':')
108 .ok_or_else(|| ActrUriError::InvalidAuthorityFormat(authority.to_string()))?;
109
110 let realm = realm_str
111 .parse::<u32>()
112 .map_err(|_| ActrUriError::InvalidRealmId(realm_str.to_string()))?;
113
114 let (manufacturer, name) = type_part
115 .split_once('+')
116 .ok_or_else(|| ActrUriError::InvalidAuthorityFormat(authority.to_string()))?;
117
118 Ok(ActrUri {
119 realm,
120 manufacturer: manufacturer.to_string(),
121 name: name.to_string(),
122 version,
123 })
124 }
125}
126
127#[derive(Debug)]
129pub struct ActrUriBuilder {
130 realm: Option<u32>,
131 manufacturer: Option<String>,
132 name: Option<String>,
133 version: String,
134}
135
136impl Default for ActrUriBuilder {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142impl ActrUriBuilder {
143 pub fn new() -> Self {
145 Self {
146 realm: None,
147 manufacturer: None,
148 name: None,
149 version: String::new(),
150 }
151 }
152
153 pub fn realm(mut self, realm: u32) -> Self {
155 self.realm = Some(realm);
156 self
157 }
158
159 pub fn manufacturer<S: Into<String>>(mut self, manufacturer: S) -> Self {
161 self.manufacturer = Some(manufacturer.into());
162 self
163 }
164
165 pub fn name<S: Into<String>>(mut self, name: S) -> Self {
167 self.name = Some(name.into());
168 self
169 }
170
171 pub fn version<S: Into<String>>(mut self, version: S) -> Self {
173 self.version = version.into();
174 self
175 }
176
177 pub fn build(self) -> Result<ActrUri, ActrUriError> {
179 let realm = self.realm.ok_or(ActrUriError::MissingAuthority)?;
180 let manufacturer = self
181 .manufacturer
182 .ok_or(ActrUriError::InvalidAuthorityFormat(
183 "missing manufacturer".to_string(),
184 ))?;
185 let name = self.name.ok_or(ActrUriError::InvalidAuthorityFormat(
186 "missing name".to_string(),
187 ))?;
188 if self.version.is_empty() {
189 return Err(ActrUriError::InvalidAuthorityFormat(
190 "missing version".to_string(),
191 ));
192 }
193
194 Ok(ActrUri {
195 realm,
196 manufacturer,
197 name,
198 version: self.version,
199 })
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_basic_uri_parsing() {
209 let uri = "actr://101:acme+echo-service@1.0.0"
210 .parse::<ActrUri>()
211 .unwrap();
212 assert_eq!(uri.realm, 101);
213 assert_eq!(uri.manufacturer, "acme");
214 assert_eq!(uri.name, "echo-service");
215 assert_eq!(uri.version, "1.0.0");
216 }
217
218 #[test]
219 fn test_uri_builder() {
220 let uri = ActrUriBuilder::new()
221 .realm(101)
222 .manufacturer("acme")
223 .name("order-service")
224 .version("1.0.0")
225 .build()
226 .unwrap();
227
228 assert_eq!(uri.realm, 101);
229 assert_eq!(uri.manufacturer, "acme");
230 assert_eq!(uri.name, "order-service");
231 assert_eq!(uri.version, "1.0.0");
232 }
233
234 #[test]
235 fn test_uri_builder_requires_version() {
236 let result = ActrUriBuilder::new()
237 .realm(101)
238 .manufacturer("acme")
239 .name("order-service")
240 .build();
241
242 assert!(matches!(
243 result,
244 Err(ActrUriError::InvalidAuthorityFormat(msg)) if msg == "missing version"
245 ));
246 }
247
248 #[test]
249 fn test_uri_to_string() {
250 let uri = ActrUri::new(
251 101,
252 "acme".to_string(),
253 "user-service".to_string(),
254 "1.0.0".to_string(),
255 );
256 let uri_string = uri.to_string();
257 assert_eq!(uri_string, "actr://101:acme+user-service@1.0.0");
258 }
259
260 #[test]
261 fn test_invalid_scheme() {
262 let result = "http://101:acme+service@1.0.0".parse::<ActrUri>();
263 assert!(matches!(result, Err(ActrUriError::InvalidScheme(_))));
264 }
265
266 #[test]
267 fn test_missing_authority() {
268 let result = "actr://".parse::<ActrUri>();
269 assert!(matches!(result, Err(ActrUriError::MissingAuthority)));
270 }
271
272 #[test]
273 fn test_missing_version() {
274 let result = "actr://101:acme+service".parse::<ActrUri>();
275 assert!(matches!(result, Err(ActrUriError::MissingVersion)));
276 }
277
278 #[test]
279 fn test_invalid_realm_id() {
280 let result = "actr://abc:acme+service@1.0.0".parse::<ActrUri>();
281 assert!(matches!(result, Err(ActrUriError::InvalidRealmId(_))));
282 }
283
284 #[test]
285 fn test_invalid_authority_format() {
286 let result = "actr://101:acme:service@1.0.0".parse::<ActrUri>();
287 assert!(matches!(
288 result,
289 Err(ActrUriError::InvalidAuthorityFormat(_))
290 ));
291 }
292
293 #[test]
294 fn test_actor_type_method() {
295 let uri = "actr://101:acme+user-service@1.0.0"
296 .parse::<ActrUri>()
297 .unwrap();
298 assert_eq!(uri.actor_type(), "acme+user-service");
299 }
300
301 #[test]
302 fn test_roundtrip() {
303 let uri = ActrUriBuilder::new()
304 .realm(9999)
305 .manufacturer("test")
306 .name("service")
307 .version("1.0.0")
308 .build()
309 .unwrap();
310
311 let uri_str = uri.to_string();
312 let parsed = uri_str.parse::<ActrUri>().unwrap();
313 assert_eq!(uri.realm, parsed.realm);
314 assert_eq!(uri.manufacturer, parsed.manufacturer);
315 assert_eq!(uri.name, parsed.name);
316 assert_eq!(uri.version, parsed.version);
317 }
318}