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 '@v1' 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, Default)]
129pub struct ActrUriBuilder {
130 realm: Option<u32>,
131 manufacturer: Option<String>,
132 name: Option<String>,
133 version: Option<String>,
134}
135
136impl ActrUriBuilder {
137 pub fn new() -> Self {
139 Self::default()
140 }
141
142 pub fn realm(mut self, realm: u32) -> Self {
144 self.realm = Some(realm);
145 self
146 }
147
148 pub fn manufacturer<S: Into<String>>(mut self, manufacturer: S) -> Self {
150 self.manufacturer = Some(manufacturer.into());
151 self
152 }
153
154 pub fn name<S: Into<String>>(mut self, name: S) -> Self {
156 self.name = Some(name.into());
157 self
158 }
159
160 pub fn version<S: Into<String>>(mut self, version: S) -> Self {
162 self.version = Some(version.into());
163 self
164 }
165
166 pub fn build(self) -> Result<ActrUri, ActrUriError> {
168 let realm = self.realm.ok_or(ActrUriError::MissingAuthority)?;
169 let manufacturer = self
170 .manufacturer
171 .ok_or(ActrUriError::InvalidAuthorityFormat(
172 "missing manufacturer".to_string(),
173 ))?;
174 let name = self.name.ok_or(ActrUriError::InvalidAuthorityFormat(
175 "missing name".to_string(),
176 ))?;
177 let version = self.version.unwrap_or_else(|| "v1".to_string());
178
179 Ok(ActrUri {
180 realm,
181 manufacturer,
182 name,
183 version,
184 })
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_basic_uri_parsing() {
194 let uri = "actr://101:acme+echo-service@v1"
195 .parse::<ActrUri>()
196 .unwrap();
197 assert_eq!(uri.realm, 101);
198 assert_eq!(uri.manufacturer, "acme");
199 assert_eq!(uri.name, "echo-service");
200 assert_eq!(uri.version, "v1");
201 }
202
203 #[test]
204 fn test_uri_builder() {
205 let uri = ActrUriBuilder::new()
206 .realm(101)
207 .manufacturer("acme")
208 .name("order-service")
209 .version("v1")
210 .build()
211 .unwrap();
212
213 assert_eq!(uri.realm, 101);
214 assert_eq!(uri.manufacturer, "acme");
215 assert_eq!(uri.name, "order-service");
216 assert_eq!(uri.version, "v1");
217 }
218
219 #[test]
220 fn test_uri_to_string() {
221 let uri = ActrUri::new(
222 101,
223 "acme".to_string(),
224 "user-service".to_string(),
225 "v1".to_string(),
226 );
227 let uri_string = uri.to_string();
228 assert_eq!(uri_string, "actr://101:acme+user-service@v1");
229 }
230
231 #[test]
232 fn test_invalid_scheme() {
233 let result = "http://101:acme+service@v1".parse::<ActrUri>();
234 assert!(matches!(result, Err(ActrUriError::InvalidScheme(_))));
235 }
236
237 #[test]
238 fn test_missing_authority() {
239 let result = "actr://".parse::<ActrUri>();
240 assert!(matches!(result, Err(ActrUriError::MissingAuthority)));
241 }
242
243 #[test]
244 fn test_missing_version() {
245 let result = "actr://101:acme+service".parse::<ActrUri>();
246 assert!(matches!(result, Err(ActrUriError::MissingVersion)));
247 }
248
249 #[test]
250 fn test_invalid_realm_id() {
251 let result = "actr://abc:acme+service@v1".parse::<ActrUri>();
252 assert!(matches!(result, Err(ActrUriError::InvalidRealmId(_))));
253 }
254
255 #[test]
256 fn test_invalid_authority_format() {
257 let result = "actr://101:acme:service@v1".parse::<ActrUri>();
258 assert!(matches!(
259 result,
260 Err(ActrUriError::InvalidAuthorityFormat(_))
261 ));
262 }
263
264 #[test]
265 fn test_actor_type_method() {
266 let uri = "actr://101:acme+user-service@v1"
267 .parse::<ActrUri>()
268 .unwrap();
269 assert_eq!(uri.actor_type(), "acme+user-service");
270 }
271
272 #[test]
273 fn test_roundtrip() {
274 let uri = ActrUriBuilder::new()
275 .realm(9999)
276 .manufacturer("test")
277 .name("service")
278 .build()
279 .unwrap();
280
281 let uri_str = uri.to_string();
282 let parsed = uri_str.parse::<ActrUri>().unwrap();
283 assert_eq!(uri.realm, parsed.realm);
284 assert_eq!(uri.manufacturer, parsed.manufacturer);
285 assert_eq!(uri.name, parsed.name);
286 assert_eq!(uri.version, parsed.version);
287 }
288}