opendal_core/types/operator/
uri.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::collections::HashMap;
19
20use http::Uri;
21use percent_encoding::percent_decode_str;
22use url::Url;
23
24use crate::{Error, ErrorKind, Result};
25
26/// Parsed representation of an operator URI with normalized components.
27#[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    /// Build [`OperatorUri`] from a URI string plus additional options.
40    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        // Allow pure scheme (e.g. "s3") to align with from_iter semantics.
52        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    /// Normalized scheme in lowercase.
122    pub fn scheme(&self) -> &str {
123        self.scheme.as_str()
124    }
125
126    /// Name extracted from the URI authority, if present.
127    pub fn name(&self) -> Option<&str> {
128        self.name.as_deref()
129    }
130
131    /// Authority extracted from the URI, if present (host with optional port).
132    pub fn authority(&self) -> Option<&str> {
133        self.authority.as_deref()
134    }
135
136    /// Username extracted from the URI, if present.
137    pub fn username(&self) -> Option<&str> {
138        self.username.as_deref()
139    }
140
141    /// Password extracted from the URI, if present.
142    pub fn password(&self) -> Option<&str> {
143        self.password.as_deref()
144    }
145
146    /// Root path (without leading slash) extracted from the URI path, if present.
147    pub fn root(&self) -> Option<&str> {
148        self.root.as_deref()
149    }
150
151    /// Normalized option map merged from query string and extra options (excluding reserved keys).
152    pub fn options(&self) -> &HashMap<String, String> {
153        &self.options
154    }
155
156    /// Retrieve a specific option by key (case-insensitive).
157    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
164/// Conversion trait that builds [`OperatorUri`] from various inputs.
165pub trait IntoOperatorUri {
166    /// Convert the input into an [`OperatorUri`].
167    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}