Skip to main content

qstring/
lib.rs

1#![warn(clippy::all)]
2
3use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS};
4use std::iter::Iterator;
5
6/// A query string. Holds a list of `(key,value)`.
7///
8/// Examples
9///
10/// Parameters can be get by their names.
11///
12/// ```
13/// let qs = qstring::QString::from("?foo=bar%20baz");
14/// let foo = qs.get("foo").unwrap();
15/// assert_eq!(foo, "bar baz");
16/// ```
17///
18/// Parameters not found are `None`.
19///
20/// ```
21/// let qs = qstring::QString::from("?foo=bar");
22/// let foo = &qs.get("panda");
23/// assert!(foo.is_none());
24/// ```
25///
26/// The query string can be assembled from pairs.
27///
28/// ```
29/// let qs = qstring::QString::new(vec![
30///    ("foo", "bar baz"),
31///    ("panda", "true"),
32/// ]);
33/// assert_eq!(format!("{}", qs), "foo=bar%20baz&panda=true");
34/// ```
35///
36#[derive(Clone, Debug, PartialEq, Default)]
37pub struct QString {
38    pairs: Vec<(String, QValue)>,
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub enum QValue {
43    Empty,
44    Value(String),
45}
46
47impl From<String> for QValue {
48    fn from(s: String) -> QValue {
49        QValue::Value(s)
50    }
51}
52
53impl QString {
54    /// Constructs a `QString` from a list of pairs.
55    ///
56    /// ```
57    /// let qs = qstring::QString::new(vec![
58    ///    ("foo", "bar baz"),
59    ///    ("panda", "true"),
60    /// ]);
61    /// assert_eq!(format!("{}", qs), "foo=bar%20baz&panda=true");
62    /// ```
63    pub fn new<S, T>(params: Vec<(S, T)>) -> QString
64    where
65        S: Into<String>,
66        T: Into<String>,
67    {
68        QString {
69            pairs: params
70                .into_iter()
71                .map(|(k, v)| (k.into(), QValue::Value(v.into())))
72                .collect(),
73        }
74    }
75
76    /// Tells if a query parameter is present.
77    ///
78    /// ```
79    /// let qs = qstring::QString::from("?foo");
80    /// assert!(qs.has("foo"));
81    /// assert!(qs.get("foo").is_some());
82    /// ```
83    pub fn has(&self, name: &str) -> bool {
84        self.pairs.iter().any(|p| p.0 == name)
85    }
86
87    /// Get a query parameter by name.
88    ///
89    /// Empty query parameters (`?foo`) return `""`
90    ///
91    /// ```
92    /// let qs = qstring::QString::from("?foo=bar");
93    /// let foo = qs.get("foo");
94    /// assert_eq!(foo, Some("bar"));
95    /// ```
96    pub fn get<'a>(&'a self, name: &str) -> Option<&'a str> {
97        self.pairs
98            .iter()
99            .find(|p| p.0 == name)
100            .and_then(|p| match p.1 {
101                QValue::Empty => Some(""),
102                QValue::Value(ref s) => Some(s),
103            })
104    }
105
106    /// Converts the QString to list of pairs.
107    ///
108    /// ```
109    /// let qs = qstring::QString::from("?foo=bar&baz=boo");
110    /// let ps = qs.into_pairs();
111    /// assert_eq!(ps, vec![
112    ///     ("foo".to_string(), "bar".to_string()),
113    ///     ("baz".to_string(), "boo".to_string()),
114    /// ]);
115    /// ```
116    pub fn into_pairs(self) -> Vec<(String, String)> {
117        self.pairs
118            .into_iter()
119            .map(|p| {
120                (
121                    p.0,
122                    match p.1 {
123                        QValue::Empty => "".to_string(),
124                        QValue::Value(s) => s,
125                    },
126                )
127            })
128            .collect()
129    }
130
131    /// Represent the QString as a list of pairs.
132    ///
133    /// ```
134    /// let qs = qstring::QString::from("?foo=bar&baz=boo");
135    /// let ps = qs.to_pairs();
136    /// assert_eq!(ps, vec![
137    ///     ("foo", "bar"),
138    ///     ("baz", "boo"),
139    /// ]);
140    /// ```
141    pub fn to_pairs(&self) -> Vec<(&str, &str)> {
142        self.pairs
143            .iter()
144            .map(|p| {
145                (
146                    p.0.as_str(),
147                    match p.1 {
148                        QValue::Empty => "",
149                        QValue::Value(ref s) => s.as_str(),
150                    },
151                )
152            })
153            .collect()
154    }
155
156    /// Adds another query parameter pair.
157    ///
158    /// ```
159    /// let mut qs = qstring::QString::from("?foo=bar&baz=boo");
160    ///
161    /// qs.add_pair(("panda", "bear"));
162    ///
163    /// assert_eq!(qs.to_string(), "foo=bar&baz=boo&panda=bear");
164    /// ```
165    pub fn add_pair<S, T>(&mut self, pair: (S, T))
166    where
167        S: Into<String>,
168        T: Into<String>,
169    {
170        self.pairs
171            .push((pair.0.into(), QValue::Value(pair.1.into())));
172    }
173
174    /// Parse the string and add all found parameters to this instance.
175    ///
176    /// ```
177    /// let mut qs = qstring::QString::from("?foo");
178    ///
179    /// qs.add_str("&bar=baz&pooch&panda=bear");
180    ///
181    /// assert_eq!(qs.to_string(), "foo&bar=baz&pooch&panda=bear");
182    /// ```
183    pub fn add_str(&mut self, origin: &str) {
184        let mut to_add = str_to_pairs(origin);
185        self.pairs.append(&mut to_add);
186    }
187
188    /// The number of query string pairs.
189    pub fn len(&self) -> usize {
190        self.pairs.len()
191    }
192
193    /// if this query string is empty.
194    pub fn is_empty(&self) -> bool {
195        self.pairs.is_empty()
196    }
197}
198
199impl<'a> From<&'a str> for QString {
200    /// Constructs a new `QString` by parsing a query string part of the URL.
201    /// Can start with ? or not, either works.
202    ///
203    /// Examples
204    ///
205    /// ```
206    /// let qs = qstring::QString::from("?foo=bar");
207    /// let v: Vec<(String, String)> = qs.into_pairs();
208    /// assert_eq!(v, vec![("foo".to_string(), "bar".to_string())]);
209    /// ```
210    fn from(origin: &str) -> Self {
211        QString {
212            pairs: str_to_pairs(origin),
213        }
214    }
215}
216
217fn str_to_pairs(origin: &str) -> Vec<(String, QValue)> {
218    // current slice left to find params in
219    let mut cur = origin;
220
221    // move forward if start with ?
222    if !cur.is_empty() && &cur[0..1] == "?" {
223        cur = &cur[1..];
224    }
225
226    // where we build found parameters into
227    let mut params = vec![];
228
229    while !cur.is_empty() {
230        // if we're positioned on a &, skip it
231        if &cur[0..1] == "&" {
232            cur = &cur[1..];
233            continue;
234        }
235        // find position of next =
236        let (name, rest) = match cur.find('=') {
237            // no next =, name will be until next & or until end
238            None => match cur.find('&') {
239                // no &, name is until end
240                None => {
241                    params.push((decode(&cur[..]), QValue::Empty));
242                    break;
243                }
244                // name is until next &, which means no value and shortcut
245                // to start straight after the &.
246                Some(pos) => {
247                    params.push((decode(&cur[..pos]), QValue::Empty));
248                    cur = &cur[(pos + 1)..];
249                    continue;
250                }
251            },
252            Some(pos) => {
253                if let Some(apos) = cur.find('&') {
254                    if apos < pos {
255                        params.push((decode(&cur[..apos]), QValue::Empty));
256                        cur = &cur[(apos + 1)..];
257                        continue;
258                    }
259                }
260                (&cur[..pos], &cur[(pos + 1)..])
261            }
262        };
263        // skip parameters with no name
264        if name.is_empty() {
265            cur = rest;
266            continue;
267        }
268        // from rest, find next occurence of &
269        let (value, newcur) = match rest.find('&') {
270            // no next &, then value is all up until end
271            None => (rest, ""),
272            // found one, value is up until & and next round starts after.
273            Some(pos) => (&rest[..pos], &rest[(pos + 1)..]),
274        };
275        // found a parameter
276        params.push((decode(name), QValue::Value(decode(value))));
277        cur = newcur;
278    }
279    params
280}
281
282impl IntoIterator for QString {
283    type Item = (String, String);
284    type IntoIter = ::std::vec::IntoIter<(String, String)>;
285    fn into_iter(self) -> Self::IntoIter {
286        self.into_pairs().into_iter()
287    }
288}
289
290impl Into<Vec<(String, String)>> for QString {
291    fn into(self) -> Vec<(String, String)> {
292        self.into_pairs()
293    }
294}
295
296impl Into<String> for QString {
297    fn into(self) -> String {
298        format!("{}", self)
299    }
300}
301
302impl ::std::fmt::Display for QString {
303    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
304        for (idx, p) in self.pairs.iter().enumerate() {
305            write!(
306                f,
307                "{}{}{}",
308                (if idx == 0 { "" } else { "&" }),
309                encode(&p.0),
310                match p.1 {
311                    QValue::Empty => "".to_string(),
312                    QValue::Value(ref s) => format!("={}", encode(s)),
313                }
314            )?;
315        }
316        Ok(())
317    }
318}
319
320fn decode(s: &str) -> String {
321    percent_decode(s.as_bytes())
322        .decode_utf8()
323        .map(|cow| cow.into_owned())
324        .unwrap_or_else(|_| s.to_string())
325}
326
327const FRAGMENT: &AsciiSet = &CONTROLS
328    .add(b' ')
329    .add(b'"')
330    .add(b'<')
331    .add(b'>')
332    .add(b'`')
333    .add(b'&')
334    .add(b'?')
335    .add(b'=');
336
337fn encode(s: &str) -> String {
338    utf8_percent_encode(s, FRAGMENT).to_string()
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    macro_rules! test {
346        ($func_name:ident, $origin:expr, $result:expr) => {
347            #[test]
348            fn $func_name() {
349                let qs = QString::from($origin);
350                let ps: Vec<(String, String)> = qs.into_pairs();
351                let cs: Vec<(String, String)> = ($result as Vec<(&str, &str)>)
352                    .into_iter()
353                    .map(|(k, v)| (k.to_string(), v.to_string()))
354                    .collect();
355                assert_eq!(ps, cs);
356            }
357        };
358    }
359
360    #[test]
361    fn encode_amp() {
362        let x = QString::new(vec![("foo", "b&?=ar")]);
363        assert_eq!("foo=b%26%3F%3Dar", x.to_string());
364    }
365
366    #[test]
367    fn amps_in_a_row() {
368        assert_eq!(
369            QString::from("&bar=baz&pooch&panda=bear").to_pairs(),
370            vec![("bar", "baz"), ("pooch", ""), ("panda", "bear")]
371        );
372    }
373
374    test!(empty_1, "", vec![]);
375    test!(empty_2, "?", vec![]);
376    test!(empty_3, "&", vec![]);
377    test!(empty_4, "=", vec![]);
378    test!(empty_5, "?=", vec![]);
379    test!(empty_6, "?&", vec![]);
380
381    test!(a_is_1, "a", vec![("a", "")]);
382    test!(a_is_2, "a=", vec![("a", "")]);
383    test!(a_is_3, "a=b", vec![("a", "b")]);
384    test!(a_is_4, "?a", vec![("a", "")]);
385    test!(a_is_5, "?a=", vec![("a", "")]);
386    test!(a_is_6, "?a=b", vec![("a", "b")]);
387    test!(a_is_7, "?&a", vec![("a", "")]);
388    test!(a_is_8, "?&a=", vec![("a", "")]);
389    test!(a_is_9, "?&a=b", vec![("a", "b")]);
390    test!(a_is_10, "?a=&", vec![("a", "")]);
391    test!(a_is_11, "?=a", vec![("a", "")]);
392
393    test!(a_is_eq_1, "a==", vec![("a", "=")]);
394
395    test!(is_q_1, "??", vec![("?", "")]);
396    test!(is_q_2, "&?", vec![("?", "")]);
397    test!(is_q_3, "??a", vec![("?a", "")]);
398    test!(is_q_4, "&?a", vec![("?a", "")]);
399
400    test!(ac_is_1, "?a&c", vec![("a", ""), ("c", "")]);
401    test!(ac_is_2, "?a&c&", vec![("a", ""), ("c", "")]);
402    test!(ac_is_3, "?a=&c", vec![("a", ""), ("c", "")]);
403    test!(ac_is_4, "?a=&c=", vec![("a", ""), ("c", "")]);
404    test!(ac_is_5, "?a=b&c=", vec![("a", "b"), ("c", "")]);
405    test!(ac_is_6, "?a=&c=d", vec![("a", ""), ("c", "d")]);
406    test!(ac_is_7, "?a=b&c=d", vec![("a", "b"), ("c", "d")]);
407}