Skip to main content

actr_protocol/
uri.rs

1//! # Actor-RTC URI parsing library
2//!
3//! Provides standard URI parsing for the actr:// protocol, without business logic.
4//!
5//! URI format: actr://<realm>:<manufacturer>+<name>@<version>
6//! Example: actr://101:acme+echo-service@1.0.0
7
8use serde::{Deserialize, Serialize};
9use std::fmt::{Display, Formatter};
10use std::str::FromStr;
11use thiserror::Error;
12
13/// Actor-RTC URI parse error
14#[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/// Actor-RTC URI structure
36/// Format: actr://<realm>:<manufacturer>+<name>@<version>
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct ActrUri {
39    /// Realm ID (u32)
40    pub realm: u32,
41    /// Manufacturer name
42    pub manufacturer: String,
43    /// Actor type name
44    pub name: String,
45    /// Version (e.g., "1.0.0")
46    pub version: String,
47}
48
49impl ActrUri {
50    /// Create a new Actor-RTC URI
51    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    /// Get scheme info
61    pub fn scheme(&self) -> &'static str {
62        "actr"
63    }
64
65    /// Get actor type string representation (manufacturer+name)
66    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        // Check for empty authority
94        if without_scheme.is_empty() {
95            return Err(ActrUriError::MissingAuthority);
96        }
97
98        // Check for version suffix '@'
99        let (authority, version) = without_scheme
100            .rsplit_once('@')
101            .ok_or(ActrUriError::MissingVersion)?;
102
103        let version = version.to_string();
104
105        // Parse realm:manufacturer+name
106        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/// Actor-RTC URI builder
128#[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    /// Create a new builder
144    pub fn new() -> Self {
145        Self {
146            realm: None,
147            manufacturer: None,
148            name: None,
149            version: String::new(),
150        }
151    }
152
153    /// Set Realm ID
154    pub fn realm(mut self, realm: u32) -> Self {
155        self.realm = Some(realm);
156        self
157    }
158
159    /// Set Manufacturer
160    pub fn manufacturer<S: Into<String>>(mut self, manufacturer: S) -> Self {
161        self.manufacturer = Some(manufacturer.into());
162        self
163    }
164
165    /// Set Actor type name
166    pub fn name<S: Into<String>>(mut self, name: S) -> Self {
167        self.name = Some(name.into());
168        self
169    }
170
171    /// Set version
172    pub fn version<S: Into<String>>(mut self, version: S) -> Self {
173        self.version = version.into();
174        self
175    }
176
177    /// Build the URI
178    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}