Skip to main content

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::parse::Error::InvalidQuery;
120    use crate::{Param, Query};
121
122    #[test]
123    fn new() {
124        let query: Query = unsafe { Query::new("?the&query=params") };
125        assert_eq!(query.query, "?the&query=params");
126    }
127
128    #[test]
129    fn try_from_str() {
130        assert_eq!(Query::try_from("?").unwrap().as_str(), "?");
131        assert_eq!(
132            Query::try_from("?key=value").unwrap().as_str(),
133            "?key=value"
134        );
135        assert_eq!(Query::try_from(""), Err(InvalidQuery));
136        assert_eq!(Query::try_from("no-question"), Err(InvalidQuery));
137        assert_eq!(Query::try_from("?#"), Err(InvalidQuery));
138    }
139
140    #[test]
141    fn is_valid() {
142        let test_cases: &[(&str, bool)] = &[
143            ("", false),
144            ("?", true),
145            ("?#", false),
146            ("?&/=!@$%^&*()", true),
147            ("?azAZ09", true),
148        ];
149        for (query, expected) in test_cases {
150            let result: bool = Query::is_valid(query);
151            assert_eq!(result, *expected, "query={}", query);
152        }
153    }
154
155    #[test]
156    fn display() {
157        let query: Query = unsafe { Query::new("?the&query=params") };
158        assert_eq!(query.as_str(), "?the&query=params");
159        assert_eq!(query.as_ref(), "?the&query=params");
160        assert_eq!(query.to_string(), "?the&query=params");
161    }
162
163    #[test]
164    fn iter_params() {
165        let query: Query = unsafe { Query::new("?") };
166        let result: Vec<Param> = query.iter().collect();
167        assert_eq!(result, vec![unsafe { Param::new("", None) }]);
168
169        let query: Query = unsafe { Query::new("?&") };
170        let result: Vec<Param> = query.iter().collect();
171        assert_eq!(
172            result,
173            vec![unsafe { Param::new("", None) }, unsafe {
174                Param::new("", None)
175            }]
176        );
177
178        let query: Query = unsafe { Query::new("?the&query=params") };
179        let result: Vec<Param> = query.iter().collect();
180        assert_eq!(
181            result,
182            vec![unsafe { Param::new("the", None) }, unsafe {
183                Param::new("query", Some("params"))
184            }]
185        );
186    }
187}