1use std::collections::HashMap;
19
20use http::Uri;
21use percent_encoding::percent_decode_str;
22use url::Url;
23
24use crate::{Error, ErrorKind, Result};
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct OperatorUri {
29 scheme: String,
30 authority: Option<String>,
31 name: Option<String>,
32 username: Option<String>,
33 password: Option<String>,
34 root: Option<String>,
35 options: HashMap<String, String>,
36}
37
38impl OperatorUri {
39 pub fn new(
41 base: &str,
42 extra_options: impl IntoIterator<Item = (String, String)>,
43 ) -> Result<Self> {
44 let url = Url::parse(base).map_err(|err| {
45 Error::new(ErrorKind::ConfigInvalid, "failed to parse uri").set_source(err)
46 })?;
47
48 let scheme = url.scheme().to_ascii_lowercase();
49
50 let mut options = HashMap::<String, String>::new();
51
52 for (key, value) in url.query_pairs() {
53 options.insert(key.to_ascii_lowercase(), value.into_owned());
54 }
55
56 for (key, value) in extra_options {
57 options.insert(key.to_ascii_lowercase(), value);
58 }
59
60 let username = if url.username().is_empty() {
61 None
62 } else {
63 Some(url.username().to_string())
64 };
65
66 let password = url.password().map(|pwd| pwd.to_string());
67
68 let authority = url.host_str().filter(|host| !host.is_empty()).map(|host| {
69 if let Some(port) = url.port() {
70 format!("{host}:{port}")
71 } else {
72 host.to_string()
73 }
74 });
75
76 let name = url
77 .host_str()
78 .filter(|host| !host.is_empty())
79 .map(|host| host.to_string());
80
81 let decoded_path = percent_decode_str(url.path()).decode_utf8_lossy();
82 let trimmed = decoded_path.trim_matches('/');
83 let root = if trimmed.is_empty() {
84 None
85 } else {
86 Some(trimmed.to_string())
87 };
88
89 Ok(Self {
90 scheme,
91 authority,
92 name,
93 username,
94 password,
95 root,
96 options,
97 })
98 }
99
100 pub fn scheme(&self) -> &str {
102 self.scheme.as_str()
103 }
104
105 pub fn name(&self) -> Option<&str> {
107 self.name.as_deref()
108 }
109
110 pub fn authority(&self) -> Option<&str> {
112 self.authority.as_deref()
113 }
114
115 pub fn username(&self) -> Option<&str> {
117 self.username.as_deref()
118 }
119
120 pub fn password(&self) -> Option<&str> {
122 self.password.as_deref()
123 }
124
125 pub fn root(&self) -> Option<&str> {
127 self.root.as_deref()
128 }
129
130 pub fn options(&self) -> &HashMap<String, String> {
132 &self.options
133 }
134
135 pub fn option(&self, key: &str) -> Option<&str> {
137 self.options
138 .get(&key.to_ascii_lowercase())
139 .map(String::as_str)
140 }
141}
142
143pub trait IntoOperatorUri {
145 fn into_operator_uri(self) -> Result<OperatorUri>;
147}
148
149impl IntoOperatorUri for OperatorUri {
150 fn into_operator_uri(self) -> Result<OperatorUri> {
151 Ok(self)
152 }
153}
154
155impl IntoOperatorUri for &OperatorUri {
156 fn into_operator_uri(self) -> Result<OperatorUri> {
157 Ok(self.clone())
158 }
159}
160
161impl IntoOperatorUri for Uri {
162 fn into_operator_uri(self) -> Result<OperatorUri> {
163 let serialized = self.to_string();
164 OperatorUri::new(&serialized, Vec::<(String, String)>::new())
165 }
166}
167
168impl IntoOperatorUri for &Uri {
169 fn into_operator_uri(self) -> Result<OperatorUri> {
170 let serialized = self.to_string();
171 OperatorUri::new(&serialized, Vec::<(String, String)>::new())
172 }
173}
174
175impl IntoOperatorUri for &str {
176 fn into_operator_uri(self) -> Result<OperatorUri> {
177 OperatorUri::new(self, Vec::<(String, String)>::new())
178 }
179}
180
181impl IntoOperatorUri for String {
182 fn into_operator_uri(self) -> Result<OperatorUri> {
183 OperatorUri::new(&self, Vec::<(String, String)>::new())
184 }
185}
186
187impl<O, K, V> IntoOperatorUri for (Uri, O)
188where
189 O: IntoIterator<Item = (K, V)>,
190 K: Into<String>,
191 V: Into<String>,
192{
193 fn into_operator_uri(self) -> Result<OperatorUri> {
194 let (uri, extra) = self;
195 let serialized = uri.to_string();
196 let opts = extra
197 .into_iter()
198 .map(|(k, v)| (k.into(), v.into()))
199 .collect::<Vec<_>>();
200 OperatorUri::new(&serialized, opts)
201 }
202}
203
204impl<O, K, V> IntoOperatorUri for (&Uri, O)
205where
206 O: IntoIterator<Item = (K, V)>,
207 K: Into<String>,
208 V: Into<String>,
209{
210 fn into_operator_uri(self) -> Result<OperatorUri> {
211 let (uri, extra) = self;
212 let serialized = uri.to_string();
213 let opts = extra
214 .into_iter()
215 .map(|(k, v)| (k.into(), v.into()))
216 .collect::<Vec<_>>();
217 OperatorUri::new(&serialized, opts)
218 }
219}
220
221impl<O, K, V> IntoOperatorUri for (&str, O)
222where
223 O: IntoIterator<Item = (K, V)>,
224 K: Into<String>,
225 V: Into<String>,
226{
227 fn into_operator_uri(self) -> Result<OperatorUri> {
228 let (base, extra) = self;
229 let opts = extra
230 .into_iter()
231 .map(|(k, v)| (k.into(), v.into()))
232 .collect::<Vec<_>>();
233 OperatorUri::new(base, opts)
234 }
235}
236
237impl<O, K, V> IntoOperatorUri for (String, O)
238where
239 O: IntoIterator<Item = (K, V)>,
240 K: Into<String>,
241 V: Into<String>,
242{
243 fn into_operator_uri(self) -> Result<OperatorUri> {
244 let (base, extra) = self;
245 (&base[..], extra).into_operator_uri()
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::types::IntoOperatorUri;
253
254 #[test]
255 fn parse_uri_with_name_and_root() {
256 let uri = OperatorUri::new(
257 "s3://example-bucket/photos/2024",
258 Vec::<(String, String)>::new(),
259 )
260 .unwrap();
261
262 assert_eq!(uri.scheme(), "s3");
263 assert_eq!(uri.authority(), Some("example-bucket"));
264 assert_eq!(uri.name(), Some("example-bucket"));
265 assert_eq!(uri.root(), Some("photos/2024"));
266 assert!(uri.options().is_empty());
267 }
268
269 #[test]
270 fn into_operator_uri_merges_extra_options() {
271 let uri = (
272 "s3://bucket/path?region=us-east-1",
273 vec![("region", "override"), ("endpoint", "https://custom")],
274 )
275 .into_operator_uri()
276 .unwrap();
277
278 assert_eq!(uri.scheme(), "s3");
279 assert_eq!(uri.name(), Some("bucket"));
280 assert_eq!(uri.root(), Some("path"));
281 assert_eq!(
282 uri.options().get("region").map(String::as_str),
283 Some("override")
284 );
285 assert_eq!(
286 uri.options().get("endpoint").map(String::as_str),
287 Some("https://custom")
288 );
289 }
290
291 #[test]
292 fn parse_uri_with_port_preserves_authority() {
293 let uri = OperatorUri::new(
294 "http://example.com:8080/root",
295 Vec::<(String, String)>::new(),
296 )
297 .unwrap();
298
299 assert_eq!(uri.scheme(), "http");
300 assert_eq!(uri.authority(), Some("example.com:8080"));
301 assert_eq!(uri.name(), Some("example.com"));
302 assert_eq!(uri.root(), Some("root"));
303 }
304
305 #[test]
306 fn parse_uri_with_credentials_splits_authority() {
307 let uri = OperatorUri::new(
308 "https://alice:secret@example.com:8443/path",
309 Vec::<(String, String)>::new(),
310 )
311 .unwrap();
312
313 assert_eq!(uri.scheme(), "https");
314 assert_eq!(uri.authority(), Some("example.com:8443"));
315 assert_eq!(uri.username(), Some("alice"));
316 assert_eq!(uri.password(), Some("secret"));
317 assert_eq!(uri.root(), Some("path"));
318 }
319}