[go: up one dir, main page]

url2/
url2.rs

1use std::collections::HashMap;
2use url::Url;
3
4use crate::*;
5
6#[derive(Clone)]
7/// Ergonomic wrapper around the popular Url crate
8pub struct Url2(pub(crate) Box<(Url, Option<HashMap<String, String>>)>);
9
10impl Url2 {
11    // would love to use std::convert::TryFrom, except for conflicting
12    // blanket implementation: https://github.com/rust-lang/rust/issues/50133
13    /// Try to parse a utf8 slice into a Url2 instance.
14    /// May result in a UrlParseError
15    ///
16    /// # Example
17    ///
18    /// ```rust
19    /// use url2::prelude::*;
20    ///
21    /// assert_eq!(
22    ///     "Err(Url2Error(UrlParseError(RelativeUrlWithoutBase)))",
23    ///     &format!("{:?}", Url2::try_parse("")),
24    /// );
25    /// assert_eq!(
26    ///     "Ok(Url2 { url: \"none:\" })",
27    ///     &format!("{:?}", Url2::try_parse("none:")),
28    /// );
29    /// ```
30    pub fn try_parse<S: AsRef<str>>(s: S) -> Url2Result<Self> {
31        Ok(Url2::priv_new(Url::parse(s.as_ref())?))
32    }
33
34    // would love to use std::convert::From, except for conflicting
35    // blanket implementation: https://github.com/rust-lang/rust/issues/50133
36    /// Try to parse a utf8 slice into a Url2 instance.
37    /// If this results in a UrlParseError, this method will panic!
38    ///
39    /// # Example
40    ///
41    /// ```rust
42    /// use url2::prelude::*;
43    ///
44    /// assert_eq!("none:", Url2::parse("none:").as_str());
45    /// ```
46    pub fn parse<S: AsRef<str>>(s: S) -> Self {
47        Self::try_parse(s).unwrap()
48    }
49
50    /// convert this Url2 instance into a string
51    ///
52    /// # Example
53    ///
54    /// ```rust
55    /// use url2::prelude::*;
56    ///
57    /// assert_eq!("none:", Url2::default().as_str());
58    /// ```
59    pub fn into_string(self) -> String {
60        self.into()
61    }
62
63    /// Access query string entries as a unique key map.
64    ///
65    /// Url query strings support multiple instances of the same key
66    /// However, many common use-cases treat the query string
67    /// keys as unique entries in a map. An optional API viewing the
68    /// query string in this manner can be more ergonomic.
69    ///
70    /// The HashMap that backs this view is only created the first time this
71    /// function (or the following query_unique_* functions) are invoked.
72    /// If you do not use them, there is no additional overhead.
73    ///
74    /// # Example
75    ///
76    /// ```rust
77    /// use url2::prelude::*;
78    ///
79    /// let mut url = Url2::default();
80    /// url.query_unique().set_pair("a", "1").set_pair("a", "2");
81    ///
82    /// assert_eq!("none:?a=2", url.as_str());
83    /// ```
84    pub fn query_unique(&mut self) -> Url2QueryUnique {
85        self.priv_ensure_query_unique_cache();
86        Url2QueryUnique { url_ref: self }
87    }
88
89    /// When parsed as a unique map, does the query string contain given key?
90    ///
91    /// # Example
92    ///
93    /// ```rust
94    /// use url2::prelude::*;
95    ///
96    /// let mut url = Url2::parse("none:?a=1");
97    ///
98    /// assert!(url.query_unique_contains_key("a"));
99    /// assert!(!url.query_unique_contains_key("b"));
100    /// ```
101    pub fn query_unique_contains_key(&mut self, key: &str) -> bool {
102        self.priv_ensure_query_unique_cache();
103        (self.0).1.as_ref().unwrap().contains_key(key)
104    }
105
106    /// When parsed as a unique map, get the value for given key
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// use url2::prelude::*;
112    ///
113    /// let mut url = Url2::parse("none:?a=1");
114    ///
115    /// assert_eq!(
116    ///     "Some(\"1\")",
117    ///     &format!("{:?}", url.query_unique_get("a")),
118    /// );
119    /// assert_eq!(
120    ///     "None",
121    ///     &format!("{:?}", url.query_unique_get("b")),
122    /// );
123    /// ```
124    pub fn query_unique_get(&mut self, key: &str) -> Option<&str> {
125        self.priv_ensure_query_unique_cache();
126        match (self.0).1.as_ref().unwrap().get(key) {
127            None => None,
128            // silly dance to convert &String to &str
129            Some(s) => Some(s),
130        }
131    }
132
133    // -- private -- //
134
135    /// private constructor, you probably want `Url2::try_parse()`
136    fn priv_new(url: Url) -> Self {
137        Self(Box::new((url, None)))
138    }
139
140    /// generate our unique query string entry cache if we haven't already
141    fn priv_ensure_query_unique_cache(&mut self) {
142        if (self.0).1.is_none() {
143            let _ = std::mem::replace(&mut (self.0).1, Some(HashMap::new()));
144            for (k, v) in (self.0).0.query_pairs() {
145                (self.0)
146                    .1
147                    .as_mut()
148                    .unwrap()
149                    .insert(k.to_string(), v.to_string());
150            }
151        }
152    }
153
154    /// if changes have been made to our query unique cache, apply them
155    pub(crate) fn priv_sync_query_unique_cache(&mut self) {
156        let mut all = (self.0).1.as_mut().unwrap().drain().collect::<Vec<_>>();
157        {
158            let mut pairs = self.query_pairs_mut();
159            pairs.clear();
160            for (k, v) in all.iter() {
161                pairs.append_pair(k, v);
162            }
163        }
164        for (k, v) in all.drain(..) {
165            (self.0).1.as_mut().unwrap().insert(k, v);
166        }
167    }
168}
169
170impl serde::Serialize for Url2 {
171    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
172    where
173        S: serde::Serializer,
174    {
175        (self.0).0.serialize(serializer)
176    }
177}
178
179impl<'de> serde::Deserialize<'de> for Url2 {
180    fn deserialize<D>(deserializer: D) -> Result<Url2, D::Error>
181    where
182        D: serde::Deserializer<'de>,
183    {
184        let url: Url = serde::Deserialize::deserialize(deserializer)?;
185        Ok(Url2::priv_new(url))
186    }
187}
188
189impl std::fmt::Debug for Url2 {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.debug_struct("Url2")
192            .field("url", &(self.0).0.as_str())
193            .finish()
194    }
195}
196
197impl std::cmp::PartialOrd for Url2 {
198    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
199        (self.0).0.partial_cmp(&(other.0).0)
200    }
201}
202
203impl std::cmp::PartialOrd<Url> for Url2 {
204    fn partial_cmp(&self, other: &Url) -> Option<std::cmp::Ordering> {
205        (self.0).0.partial_cmp(other)
206    }
207}
208
209impl std::cmp::Ord for Url2 {
210    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
211        (self.0).0.cmp(&(other.0).0)
212    }
213}
214
215impl std::cmp::PartialEq for Url2 {
216    fn eq(&self, other: &Self) -> bool {
217        (self.0).0.eq(&(other.0).0)
218    }
219}
220
221impl std::cmp::Eq for Url2 {}
222
223impl std::cmp::PartialEq<Url> for Url2 {
224    fn eq(&self, other: &Url) -> bool {
225        (self.0).0.eq(&other)
226    }
227}
228
229impl std::hash::Hash for Url2 {
230    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
231        (self.0).0.hash(state);
232    }
233}
234
235impl std::fmt::Display for Url2 {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        write!(f, "{}", (self.0).0)
238    }
239}
240
241impl std::convert::From<Url2> for String {
242    fn from(url: Url2) -> String {
243        url.to_string()
244    }
245}
246
247impl std::default::Default for Url2 {
248    fn default() -> Self {
249        Url2::priv_new(Url::parse("none:").unwrap())
250    }
251}
252
253impl std::convert::AsRef<str> for Url2 {
254    fn as_ref(&self) -> &str {
255        (self.0).0.as_ref()
256    }
257}
258
259impl std::borrow::Borrow<str> for Url2 {
260    fn borrow(&self) -> &str {
261        (self.0).0.as_ref()
262    }
263}
264
265impl std::ops::Deref for Url2 {
266    type Target = Url;
267
268    fn deref(&self) -> &Self::Target {
269        &(self.0).0
270    }
271}
272
273impl std::ops::DerefMut for Url2 {
274    fn deref_mut(&mut self) -> &mut Self::Target {
275        &mut (self.0).0
276    }
277}
278
279impl std::borrow::Borrow<Url> for Url2 {
280    fn borrow(&self) -> &Url {
281        &(self.0).0
282    }
283}
284
285impl std::borrow::BorrowMut<Url> for Url2 {
286    fn borrow_mut(&mut self) -> &mut Url {
287        &mut (self.0).0
288    }
289}
290
291impl std::convert::AsRef<Url> for Url2 {
292    fn as_ref(&self) -> &Url {
293        &(self.0).0
294    }
295}
296
297impl std::convert::AsMut<Url> for Url2 {
298    fn as_mut(&mut self) -> &mut Url {
299        &mut (self.0).0
300    }
301}
302
303impl std::convert::From<Url> for Url2 {
304    fn from(url: Url) -> Url2 {
305        Url2::priv_new(url)
306    }
307}
308
309impl std::convert::From<&Url> for Url2 {
310    fn from(url: &Url) -> Url2 {
311        Url2::priv_new(url.clone())
312    }
313}
314
315impl std::convert::From<Url2> for Url {
316    fn from(url: Url2) -> Url {
317        (url.0).0
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn it_is_small_for_enum_usage() {
327        assert_eq!(std::mem::size_of::<usize>(), std::mem::size_of::<Url2>());
328    }
329
330    #[test]
331    fn it_can_serialize_deserialize() {
332        let url = Url2::parse("s://u:p@h:42/a/b?a=b&c=d#e");
333        let json = serde_json::to_string(&url).unwrap();
334        assert_eq!("\"s://u:p@h:42/a/b?a=b&c=d#e\"", json);
335        let de: Url2 = serde_json::from_str(&json).unwrap();
336        assert_eq!(url, de);
337    }
338
339    #[test]
340    fn it_can_display() {
341        assert_eq!("test:foo", &format!("{}", Url2::parse("test:foo")));
342        assert_eq!("test:foo", &Url2::parse("test:foo").into_string());
343    }
344
345    #[test]
346    fn it_can_parse() {
347        let url_a = Url2::try_parse("test:bob").unwrap();
348        let url_b = Url2::parse("test:bob");
349        let url_c = try_url2!("{}:{}", "test", "bob").unwrap();
350        let url_d = url2!("{}:{}", "test", "bob");
351        assert_eq!(url_a, url_b);
352        assert_eq!(url_a, url_c);
353        assert_eq!(url_a, url_d);
354    }
355
356    #[test]
357    fn it_can_convert_from() {
358        let url = Url2::default();
359        let url: Url = url.into();
360        let url: Url2 = url.into();
361        let url: Url = url.into();
362        let url: Url2 = (&url).into();
363        assert_eq!("none:", url.as_str());
364    }
365
366    #[test]
367    fn it_can_edit_query_unique() {
368        let mut url = Url2::default();
369        url.query_unique()
370            .set_pair("a", "test1")
371            .set_pair("b", "test2");
372        assert!(
373            "none:?a=test1&b=test2" == url.as_str()
374                || "none:?b=test2&a=test1" == url.as_str()
375        );
376        assert_eq!(true, url.query_unique_contains_key("a"));
377        assert_eq!(false, url.query_unique_contains_key("c"));
378        assert_eq!(Some("test1"), url.query_unique_get("a"));
379        assert_eq!(None, url.query_unique_get("c"));
380    }
381}