web_url/
query.rs

1use std::fmt::{Display, Formatter};
2
3use crate::parse::Error;
4use crate::parse::Error::InvalidQuery;
5use crate::Param;
6
7/// A web-based URL query.
8///
9/// # Validation
10/// A query will never be empty and will always start with a '?'.
11///
12/// The query string can contain any US-ASCII letter, number, or punctuation char excluding '#'
13/// since this char denotes the end of the query in the URL.
14#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
15pub struct Query<'a> {
16    query: &'a str,
17}
18
19impl<'a> Query<'a> {
20    //! Construction
21
22    /// Creates a new query.
23    ///
24    /// # Safety
25    /// The `query` must be valid.
26    pub unsafe fn new(query: &'a str) -> Self {
27        debug_assert!(Self::is_valid(query));
28
29        Self { query }
30    }
31}
32
33impl<'a> TryFrom<&'a str> for Query<'a> {
34    type Error = Error;
35
36    fn try_from(query: &'a str) -> Result<Self, Self::Error> {
37        if Self::is_valid(query) {
38            Ok(Self { query })
39        } else {
40            Err(InvalidQuery)
41        }
42    }
43}
44
45impl<'a> Query<'a> {
46    //! Validation
47
48    /// Checks if the `query` is valid.
49    pub fn is_valid(query: &str) -> bool {
50        !query.is_empty()
51            && query.as_bytes()[0] == b'?'
52            && query.as_bytes()[1..]
53                .iter()
54                .all(|c| c.is_ascii_alphanumeric() || (c.is_ascii_punctuation() && *c != b'#'))
55    }
56}
57
58impl<'a> Query<'a> {
59    //! String
60
61    /// Gets the query string.
62    pub const fn as_str(&self) -> &str {
63        self.query
64    }
65}
66
67impl<'a> AsRef<str> for Query<'a> {
68    fn as_ref(&self) -> &str {
69        self.query
70    }
71}
72
73impl<'a> Display for Query<'a> {
74    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}", self.query)
76    }
77}
78
79impl<'a> Query<'a> {
80    //! Iteration
81
82    /// Creates a new iterator for the query parameters.
83    pub const fn iter(&self) -> impl Iterator<Item = Param<'a>> {
84        ParamIterator {
85            remaining: self.query,
86        }
87    }
88}
89
90/// Responsible for iterating over query parameters.
91#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
92struct ParamIterator<'a> {
93    remaining: &'a str,
94}
95
96impl<'a> Iterator for ParamIterator<'a> {
97    type Item = Param<'a>;
98
99    fn next(&mut self) -> Option<Self::Item> {
100        if self.remaining.is_empty() {
101            None
102        } else {
103            self.remaining = &self.remaining[1..];
104            if let Some(amp) = self.remaining.as_bytes().iter().position(|c| *c == b'&') {
105                let result: Param = unsafe { Param::from_str(&self.remaining[..amp]) };
106                self.remaining = &self.remaining[amp..];
107                Some(result)
108            } else {
109                let result: Param = unsafe { Param::from_str(self.remaining) };
110                self.remaining = "";
111                Some(result)
112            }
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::{Param, Query};
120
121    #[test]
122    fn new() {
123        let query: Query = unsafe { Query::new("?the&query=params") };
124        assert_eq!(query.query, "?the&query=params");
125    }
126
127    #[test]
128    fn is_valid() {
129        let test_cases: &[(&str, bool)] = &[
130            ("", false),
131            ("?", true),
132            ("?#", false),
133            ("?&/=!@$%^&*()", true),
134            ("?azAZ09", true),
135        ];
136        for (query, expected) in test_cases {
137            let result: bool = Query::is_valid(query);
138            assert_eq!(result, *expected, "query={}", query);
139        }
140    }
141
142    #[test]
143    fn display() {
144        let query: Query = unsafe { Query::new("?the&query=params") };
145        assert_eq!(query.as_str(), "?the&query=params");
146        assert_eq!(query.as_ref(), "?the&query=params");
147        assert_eq!(query.to_string(), "?the&query=params");
148    }
149
150    #[test]
151    fn iter_params() {
152        let query: Query = unsafe { Query::new("?") };
153        let result: Vec<Param> = query.iter().collect();
154        assert_eq!(result, vec![unsafe { Param::new("", None) }]);
155
156        let query: Query = unsafe { Query::new("?&") };
157        let result: Vec<Param> = query.iter().collect();
158        assert_eq!(
159            result,
160            vec![unsafe { Param::new("", None) }, unsafe {
161                Param::new("", None)
162            }]
163        );
164
165        let query: Query = unsafe { Query::new("?the&query=params") };
166        let result: Vec<Param> = query.iter().collect();
167        assert_eq!(
168            result,
169            vec![unsafe { Param::new("the", None) }, unsafe {
170                Param::new("query", Some("params"))
171            }]
172        );
173    }
174}