opendal/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;
22
23use crate::{Error, ErrorKind, Result};
24
25/// Parsed representation of an operator URI with normalized components.
26#[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    /// Build [`OperatorUri`] from a [`Uri`] plus additional options.
37    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    /// Normalized scheme in lowercase.
100    pub fn scheme(&self) -> &str {
101        self.scheme.as_str()
102    }
103
104    /// Name extracted from the URI authority, if present.
105    pub fn name(&self) -> Option<&str> {
106        self.name.as_deref()
107    }
108
109    /// Authority extracted from the URI, if present (host with optional port).
110    pub fn authority(&self) -> Option<&str> {
111        self.authority.as_deref()
112    }
113
114    /// Root path (without leading slash) extracted from the URI path, if present.
115    pub fn root(&self) -> Option<&str> {
116        self.root.as_deref()
117    }
118
119    /// Normalized option map merged from query string and extra options (excluding reserved keys).
120    pub fn options(&self) -> &HashMap<String, String> {
121        &self.options
122    }
123}
124
125/// Conversion trait that builds [`OperatorUri`] from various inputs.
126pub trait IntoOperatorUri {
127    /// Convert the input into an [`OperatorUri`].
128    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}