http_uri_ext/
lib.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use http::uri::{self, Uri};
6
7pub trait HttpUriExt {
8    /// Normalizes empty paths to `/`, appends `/` to `self`'s path if it does not end with one,
9    /// then appends `path`, preserving any query parameters. Does nothing if `path` is the empty
10    /// string.
11    ///
12    /// Will only error if asked to add a path to a `Uri` without a scheme (because `Uri` requires
13    /// a scheme if a path is present), or if `path` contains invalid URI characters.
14    fn extend_dir_with_path(self, path: &str) -> Result<Uri, Error>;
15
16    /// Append the given query parameter `key`=`value` to the URI, preserving existing query
17    /// parameters if any, `key` and `value` should already be URL-encoded (if necessary).
18    ///
19    /// Will only error if `key` or `value` contains invalid URI characters.
20    fn append_query_parameter(self, key: &str, value: &str) -> Result<Uri, Error>;
21}
22
23impl HttpUriExt for Uri {
24    fn extend_dir_with_path(self, path: &str) -> Result<Uri, Error> {
25        if path.is_empty() {
26            return Ok(self);
27        }
28        let mut base_parts = self.into_parts();
29        let (base_path, query) = match &base_parts.path_and_query {
30            Some(path_and_query) => (path_and_query.path(), path_and_query.query()),
31            None => ("/", None),
32        };
33        let new_path_and_query = if base_path.ends_with("/") {
34            if let Some(query) = query {
35                format!("{}{}?{}", base_path, path, query)
36            } else {
37                format!("{}{}", base_path, path)
38            }
39        } else {
40            if let Some(query) = query {
41                format!("{}/{}?{}", base_path, path, query)
42            } else {
43                format!("{}/{}", base_path, path)
44            }
45        };
46        base_parts.path_and_query = Some(new_path_and_query.parse()?);
47        Ok(Uri::from_parts(base_parts)?)
48    }
49
50    fn append_query_parameter(self, key: &str, value: &str) -> Result<Uri, Error> {
51        let mut base_parts = self.into_parts();
52        let new_path_and_query = match &base_parts.path_and_query {
53            Some(path_and_query) => {
54                if let Some(query) = path_and_query.query() {
55                    format!("{}?{query}&{key}={value}", path_and_query.path())
56                } else {
57                    format!("{}?{key}={value}", path_and_query.path())
58                }
59            }
60            None => format!("?{key}={value}"),
61        };
62        base_parts.path_and_query = Some(new_path_and_query.parse()?);
63        Ok(Uri::from_parts(base_parts)?)
64    }
65}
66
67#[derive(Debug, thiserror::Error)]
68pub enum Error {
69    #[error("invalid uri: {0}")]
70    InvalidUri(#[from] uri::InvalidUri),
71    #[error("invalid uri parts: {0}")]
72    InvalidUriParts(#[from] uri::InvalidUriParts),
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    fn make_uri_from_path_and_query(path_and_query: Option<&str>) -> Uri {
80        let mut parts = uri::Parts::default();
81        parts.path_and_query = path_and_query.map(|p| p.parse().unwrap());
82        Uri::from_parts(parts).unwrap()
83    }
84
85    fn assert_expected_path(base: Option<&str>, added: &str, expected: Option<&str>) {
86        let uri = make_uri_from_path_and_query(base).extend_dir_with_path(added).unwrap();
87        assert_eq!(
88            uri.into_parts().path_and_query.map(|p| p.to_string()),
89            expected.map(|s| s.to_string())
90        );
91    }
92
93    #[test]
94    fn no_query_empty_argument() {
95        assert_expected_path(None, "", None);
96        assert_expected_path(Some(""), "", None);
97        assert_expected_path(Some("/"), "", Some("/"));
98        assert_expected_path(Some("/a"), "", Some("/a"));
99        assert_expected_path(Some("/a/"), "", Some("/a/"));
100    }
101
102    #[test]
103    fn has_query_empty_argument() {
104        assert_expected_path(Some("?k=v"), "", Some("/?k=v"));
105        assert_expected_path(Some("/?k=v"), "", Some("/?k=v"));
106        assert_expected_path(Some("/a?k=v"), "", Some("/a?k=v"));
107        assert_expected_path(Some("/a/?k=v"), "", Some("/a/?k=v"));
108    }
109
110    #[test]
111    fn no_query_has_argument() {
112        assert_expected_path(None, "c", Some("/c"));
113        assert_expected_path(Some(""), "c", Some("/c"));
114        assert_expected_path(Some("/"), "c", Some("/c"));
115        assert_expected_path(Some("/a"), "c", Some("/a/c"));
116        assert_expected_path(Some("/a/"), "c", Some("/a/c"));
117    }
118
119    #[test]
120    fn has_query_has_argument() {
121        assert_expected_path(Some("?k=v"), "c", Some("/c?k=v"));
122        assert_expected_path(Some("/?k=v"), "c", Some("/c?k=v"));
123        assert_expected_path(Some("/a?k=v"), "c", Some("/a/c?k=v"));
124        assert_expected_path(Some("/a/?k=v"), "c", Some("/a/c?k=v"));
125    }
126
127    fn assert_expected_param(base: Option<&str>, key: &str, value: &str, expected: Option<&str>) {
128        let uri = make_uri_from_path_and_query(base).append_query_parameter(key, value).unwrap();
129        assert_eq!(
130            uri.into_parts().path_and_query.map(|p| p.to_string()),
131            expected.map(|s| s.to_string())
132        );
133    }
134
135    #[test]
136    fn new_query() {
137        assert_expected_param(None, "k", "v", Some("/?k=v"));
138        assert_expected_param(Some(""), "k", "v", Some("/?k=v"));
139        assert_expected_param(Some("/"), "k", "v", Some("/?k=v"));
140        assert_expected_param(Some("/a"), "k", "v", Some("/a?k=v"));
141        assert_expected_param(Some("/a/"), "k", "v", Some("/a/?k=v"));
142    }
143
144    #[test]
145    fn append_query() {
146        assert_expected_param(Some("?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
147        assert_expected_param(Some("/?k=v"), "k2", "v2", Some("/?k=v&k2=v2"));
148        assert_expected_param(Some("/a?k=v"), "k2", "v2", Some("/a?k=v&k2=v2"));
149        assert_expected_param(Some("/a/?k=v"), "k2", "v2", Some("/a/?k=v&k2=v2"));
150    }
151}