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