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