1use serde::{Deserialize, Serialize};
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8use thiserror::Error;
9
10#[derive(Error, Debug)]
12pub enum ActrUriError {
13 #[error("Invalid URI scheme, expected 'actr' but got '{0}'")]
14 InvalidScheme(String),
15
16 #[error("Missing actor type in URI")]
17 MissingActorType,
18
19 #[error("URI parse error: {0}")]
20 ParseError(String),
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct ActrUri {
27 pub actor_type: String,
29
30 pub path: Option<String>,
32
33 pub query_params: std::collections::HashMap<String, String>,
35}
36
37impl ActrUri {
38 pub fn new(actor_type: String) -> Self {
40 Self {
41 actor_type,
42 path: None,
43 query_params: std::collections::HashMap::new(),
44 }
45 }
46
47 pub fn with_path(mut self, path: String) -> Self {
49 self.path = Some(path);
50 self
51 }
52
53 pub fn with_query_param(mut self, key: String, value: String) -> Self {
55 self.query_params.insert(key, value);
56 self
57 }
58
59 pub fn scheme(&self) -> &'static str {
61 "actr"
62 }
63}
64
65impl Display for ActrUri {
66 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67 let mut uri = format!("actr://{}", self.actor_type);
68
69 if let Some(ref path) = self.path {
70 if !path.starts_with('/') {
71 uri.push('/');
72 }
73 uri.push_str(path);
74 } else {
75 uri.push('/');
76 }
77
78 if !self.query_params.is_empty() {
79 uri.push('?');
80 let params: Vec<String> = self
81 .query_params
82 .iter()
83 .map(|(k, v)| format!("{k}={v}"))
84 .collect();
85 uri.push_str(¶ms.join("&"));
86 }
87
88 write!(f, "{uri}")
89 }
90}
91
92impl FromStr for ActrUri {
93 type Err = ActrUriError;
94
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 if !s.starts_with("actr://") {
97 return Err(ActrUriError::InvalidScheme(
98 s.split(':').next().unwrap_or("").to_string(),
99 ));
100 }
101
102 let without_scheme = &s[7..];
103
104 let (base_part, query_part) = if let Some(idx) = without_scheme.find('?') {
105 (&without_scheme[..idx], Some(&without_scheme[idx + 1..]))
106 } else {
107 (without_scheme, None)
108 };
109
110 let (actor_type, path) = if let Some(idx) = base_part.find('/') {
111 let actor_type = &base_part[..idx];
112 let path_part = &base_part[idx + 1..];
113
114 if actor_type.is_empty() {
115 return Err(ActrUriError::MissingActorType);
116 }
117
118 let path = if path_part.is_empty() {
119 None
120 } else {
121 Some(path_part.to_string())
122 };
123
124 (actor_type.to_string(), path)
125 } else {
126 if base_part.is_empty() {
127 return Err(ActrUriError::MissingActorType);
128 }
129 (base_part.to_string(), None)
130 };
131
132 let mut query_params = std::collections::HashMap::new();
133 if let Some(query) = query_part {
134 for param in query.split('&') {
135 if let Some(idx) = param.find('=') {
136 let key = param[..idx].to_string();
137 let value = param[idx + 1..].to_string();
138 query_params.insert(key, value);
139 } else if !param.is_empty() {
140 query_params.insert(param.to_string(), String::new());
141 }
142 }
143 }
144
145 Ok(ActrUri {
146 actor_type,
147 path,
148 query_params,
149 })
150 }
151}
152
153#[derive(Debug, Default)]
155pub struct ActrUriBuilder {
156 actor_type: Option<String>,
157 path: Option<String>,
158 query_params: std::collections::HashMap<String, String>,
159}
160
161impl ActrUriBuilder {
162 pub fn new() -> Self {
164 Self::default()
165 }
166
167 pub fn actor_type<S: Into<String>>(mut self, actor_type: S) -> Self {
169 self.actor_type = Some(actor_type.into());
170 self
171 }
172
173 pub fn path<S: Into<String>>(mut self, path: S) -> Self {
175 self.path = Some(path.into());
176 self
177 }
178
179 pub fn query<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
181 self.query_params.insert(key.into(), value.into());
182 self
183 }
184
185 pub fn build(self) -> Result<ActrUri, ActrUriError> {
187 let actor_type = self.actor_type.ok_or(ActrUriError::MissingActorType)?;
188
189 Ok(ActrUri {
190 actor_type,
191 path: self.path,
192 query_params: self.query_params,
193 })
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_basic_uri_parsing() {
203 let uri = "actr://user-service/".parse::<ActrUri>().unwrap();
204 assert_eq!(uri.actor_type, "user-service");
205 assert_eq!(uri.path, None);
206 assert!(uri.query_params.is_empty());
207 }
208
209 #[test]
210 fn test_uri_with_path() {
211 let uri = "actr://user-service/api/v1".parse::<ActrUri>().unwrap();
212 assert_eq!(uri.actor_type, "user-service");
213 assert_eq!(uri.path, Some("api/v1".to_string()));
214 }
215
216 #[test]
217 fn test_uri_with_query_params() {
218 let uri = "actr://notification-service/?param1=value1¶m2=value2"
219 .parse::<ActrUri>()
220 .unwrap();
221 assert_eq!(uri.actor_type, "notification-service");
222 assert_eq!(uri.path, None);
223 assert_eq!(uri.query_params.get("param1"), Some(&"value1".to_string()));
224 assert_eq!(uri.query_params.get("param2"), Some(&"value2".to_string()));
225 }
226
227 #[test]
228 fn test_uri_without_trailing_slash() {
229 let uri = "actr://payment-service".parse::<ActrUri>().unwrap();
230 assert_eq!(uri.actor_type, "payment-service");
231 assert_eq!(uri.path, None);
232 }
233
234 #[test]
235 fn test_uri_builder() {
236 let uri = ActrUriBuilder::new()
237 .actor_type("order-service")
238 .path("orders/create")
239 .query("timeout", "30s")
240 .build()
241 .unwrap();
242
243 assert_eq!(uri.actor_type, "order-service");
244 assert_eq!(uri.path, Some("orders/create".to_string()));
245 assert_eq!(uri.query_params.get("timeout"), Some(&"30s".to_string()));
246 }
247
248 #[test]
249 fn test_uri_to_string() {
250 let uri = ActrUri::new("user-service".to_string())
251 .with_path("users/profile".to_string())
252 .with_query_param("format".to_string(), "json".to_string());
253
254 let uri_string = uri.to_string();
255 assert!(uri_string.starts_with("actr://user-service"));
256 assert!(uri_string.contains("users/profile"));
257 assert!(uri_string.contains("format=json"));
258 }
259
260 #[test]
261 fn test_invalid_scheme() {
262 let result = "http://user-service/".parse::<ActrUri>();
263 assert!(matches!(result, Err(ActrUriError::InvalidScheme(_))));
264 }
265
266 #[test]
267 fn test_missing_actor_type() {
268 let result = "actr:///".parse::<ActrUri>();
269 assert!(matches!(result, Err(ActrUriError::MissingActorType)));
270 }
271
272 #[test]
273 fn test_empty_query_param() {
274 let uri = "actr://service/?flag".parse::<ActrUri>().unwrap();
275 assert_eq!(uri.query_params.get("flag"), Some(&"".to_string()));
276 }
277
278 #[test]
279 fn test_complex_path() {
280 let uri = "actr://api-gateway/v2/users/123/profile"
281 .parse::<ActrUri>()
282 .unwrap();
283 assert_eq!(uri.actor_type, "api-gateway");
284 assert_eq!(uri.path, Some("v2/users/123/profile".to_string()));
285 }
286}