pathetic/
lib.rs

1//! A library for working with relative URIs, based on the `url` crate.
2//! 
3//! # Usage:
4//! 
5//! ```rust
6//! fn main() {
7//!     let uri = pathetic::Uri::default()
8//!         .with_path_segments_mut(|p| p.extend(&["foo", "bar"]))
9//!         .with_query_pairs_mut(|q| q.append_pair("foo", "bar"))
10//!         .with_fragment(Some("baz"));
11//! 
12//!     assert_eq!("/foo/bar?foo=bar#baz", uri.as_str());
13//! } 
14//! ```
15
16/// Create a new base `Uri`
17pub fn uri() -> Uri {
18    Uri::default()
19}
20
21/// The relative URI container
22#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
23pub struct Uri(url::Url);
24
25impl std::fmt::Debug for Uri {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_tuple("Uri").field(&self.as_str()).finish()
28    }
29}
30
31impl std::fmt::Display for Uri {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        self.as_str().fmt(f)
34    }
35}
36
37impl std::convert::TryFrom<&str> for Uri {
38    type Error = url::ParseError;
39    fn try_from(t: &str) -> Result<Self, Self::Error> {
40        Self::new(t)
41    }
42}
43
44#[cfg(feature = "actix-web")]
45impl From<&actix_web::http::uri::Uri> for Uri {
46    fn from(t: &actix_web::http::uri::Uri) -> Self {
47        Self::default()
48            .with_path(t.path())
49            .with_query(t.query())
50    }
51}
52
53impl AsRef<str> for Uri {
54    fn as_ref(&self) -> &str {
55        self.as_str()
56    }
57}
58
59impl Default for Uri {
60    fn default() -> Self {
61        Self(Self::base_url())
62    }
63}
64
65impl Uri {
66    fn base_url() -> url::Url {
67        use once_cell::sync::Lazy; 
68        static URL: Lazy<url::Url> =
69            Lazy::new(|| "http://_".parse().expect("`http://_` is a valid `URL`"));
70        URL.clone()
71    }
72
73    /// Create a new `Uri`, from a path-query-fragment `str`.
74    pub fn new(input: &str) -> Result<Self, url::ParseError> {
75        Self::base_url().join(input).map(Self)
76    }
77
78    /// Parse a string as an URL, with this URL as the base URL.
79    pub fn join(&self, input: &str) -> Result<Self, url::ParseError> {
80        self.0.join(input).map(Self)
81    }
82
83    /// Return the serialization of this URL.    
84    pub fn as_str(&self) -> &str {
85        &self.0[url::Position::BeforePath..]
86    }    
87
88    /// Return the path for this URL, as a percent-encoded ASCII string.
89    pub fn path(&self) -> &str {
90        self.0.path()
91    }
92
93    /// Return this URL's query string, if any, as a percent-encoded ASCII string.
94    pub fn query(&self) -> Option<&str> {
95        self.0.query()
96    }
97
98    /// Return this URL's fragment identifier, if any.
99    pub fn fragment(&self) -> Option<&str> {
100        self.0.fragment()
101    }
102
103    /// Return an iterator of '/' slash-separated path segments, each as a percent-encoded ASCII string.
104    pub fn path_segments(&self) -> std::str::Split<char> {
105        self.0.path_segments().expect("`Uri` is always-a-base")
106    }
107
108    /// Return an object with methods to manipulate this URL's path segments.
109    pub fn path_segments_mut(&mut self) -> url::PathSegmentsMut {
110        self.0.path_segments_mut().expect("`Uri` is always-a-base")
111    }
112
113    /// Parse the URL's query string, if any, as application/x-www-form-urlencoded and return an iterator of (key, value) pairs.
114    pub fn query_pairs(&self) -> url::form_urlencoded::Parse {
115        self.0.query_pairs()
116    }
117
118    /// Manipulate this URL's query string, viewed as a sequence of name/value pairs in application/x-www-form-urlencoded syntax.
119    pub fn query_pairs_mut(&mut self) -> url::form_urlencoded::Serializer<url::UrlQuery> {
120        self.0.query_pairs_mut()
121    }
122
123    /// Change this URL's path.
124    pub fn set_path(&mut self, path: &str) {
125        self.0.set_path(path)
126    }
127
128    /// Change this URL's query string.
129    pub fn set_query(&mut self, query: Option<&str>) {
130        self.0.set_query(query)
131    }
132
133    /// Change this URL's fragment identifier.
134    pub fn set_fragment(&mut self, fragment: Option<&str>) {
135        self.0.set_fragment(fragment)
136    }
137
138    /// Modify the path inline.
139    pub fn with_path(mut self, path: &str) -> Self {
140        self.set_path(path);
141        self
142    }
143
144    /// Modify the fragment inline.
145    pub fn with_query(mut self, query: Option<&str>) -> Self {
146        self.set_query(query);
147        self
148    }
149
150    /// Modify the fragment inline.
151    pub fn with_fragment(mut self, fragment: Option<&str>) -> Self {
152        self.set_fragment(fragment);
153        self
154    }
155
156    /// Modify the path segments inline.
157    pub fn with_path_segments_mut<F>(mut self, cls: F) -> Self
158    where
159        F: for<'a, 'b> Fn(&'b mut url::PathSegmentsMut<'a>) -> &'b mut url::PathSegmentsMut<'a>,
160    {
161        {
162            let mut path_segments_mut = self.path_segments_mut();
163            cls(&mut path_segments_mut);
164        }
165        self
166    }
167
168    /// Modify the query pairs inline.
169    pub fn with_query_pairs_mut<F>(mut self, cls: F) -> Self
170    where
171        F: for<'a, 'b> Fn(
172            &'b mut url::form_urlencoded::Serializer<'a, url::UrlQuery<'a>>,
173        )
174            -> &'b mut url::form_urlencoded::Serializer<'a, url::UrlQuery<'a>>,
175    {
176        {
177            let mut query_pairs_mut = self.query_pairs_mut();
178            cls(&mut query_pairs_mut);
179        }
180        self
181    }
182}
183
184#[cfg(test)]
185mod tests {
186
187    use super::*;
188
189    #[test]
190    fn it_works() {
191
192        let uri = Uri::default()
193            .with_path_segments_mut(|p| p.push("foo"));
194
195        assert_eq!("Uri(\"/foo\")", format!("{:?}", &uri));
196
197        let mut uri = Uri::new("../../../foo.html?lorem=ipsum").unwrap();
198
199        assert_eq!("/foo.html?lorem=ipsum", uri.as_str());
200
201        uri.query_pairs_mut().clear().append_pair("foo", "bar & baz");
202
203        assert_eq!("/foo.html?foo=bar+%26+baz", uri.as_str());
204
205        let mut uri = Uri::default();
206
207        uri.path_segments_mut().extend(&["foo", "bar", "baz"]);
208
209        assert_eq!("/foo/bar/baz", uri.as_str());
210
211        let uri = uri.join("/baz").unwrap();
212
213        assert_eq!("/baz", uri.as_str());
214
215        let mut uri = uri.join("?foo=bar").unwrap();
216
217        assert_eq!("/baz?foo=bar", uri.as_str());
218
219        uri.path_segments_mut().clear();
220
221        assert_eq!("/?foo=bar", uri.as_str());
222
223        let uri = Uri::default()
224            .with_path_segments_mut(|p| p.extend(&["foo", "bar"]))
225            .with_query_pairs_mut(|q| q.append_pair("foo", "bar"))
226            .with_fragment(Some("baz"));
227
228        assert_eq!("/foo/bar?foo=bar#baz", uri.as_str());
229
230        let uri = Uri::default().with_path("/a/b/c/d/e/f/g");
231
232        let uri = uri.join("../../../../..").unwrap();
233
234        assert_eq!("/a/", uri.as_str());
235
236    }
237}