Skip to main content

vcon_server/
query.rs

1//! Query strings: the one rule every route that takes parameters reads
2//! them by, so the HTTP routes and the tools built on the same functions
3//! refuse the same queries.
4
5use std::collections::BTreeMap;
6use std::fmt;
7
8/// A query the route does not take: a name it does not know, a name given
9/// twice, or a value it cannot read.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct BadQuery;
12
13impl fmt::Display for BadQuery {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        f.write_str(concat!(
16            "the query names a parameter the route does not take, ",
17            "names one twice, or gives a value it cannot read"
18        ))
19    }
20}
21
22impl std::error::Error for BadQuery {}
23
24/// The parameters of `query`, percent-decoded, by name. A name that is not
25/// in `names`, or that is given twice, is refused, so a misspelt or
26/// repeated parameter never goes unnoticed. No query is no parameters.
27///
28/// # Errors
29///
30/// [`BadQuery`] if a name is not in `names`, a name is given twice, or a
31/// piece has no `=`.
32pub fn pairs(query: Option<&str>, names: &[&str]) -> Result<BTreeMap<String, String>, BadQuery> {
33    let mut read = BTreeMap::new();
34    // Empty pieces, as a trailing `&` leaves, name nothing.
35    for piece in query.unwrap_or("").split('&').filter(|p| !p.is_empty()) {
36        if !piece.contains('=') {
37            return Err(BadQuery);
38        }
39        let (name, value) = form_urlencoded::parse(piece.as_bytes())
40            .next()
41            .ok_or(BadQuery)?;
42        if !names.contains(&name.as_ref()) {
43            return Err(BadQuery);
44        }
45        if read.insert(name.into_owned(), value.into_owned()).is_some() {
46            return Err(BadQuery);
47        }
48    }
49    Ok(read)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    const NAMES: &[&str] = &["limit", "cursor", "since"];
57
58    fn read(query: &str) -> Result<Vec<(String, String)>, BadQuery> {
59        pairs(Some(query), NAMES).map(|p| p.into_iter().collect())
60    }
61
62    fn pair(name: &str, value: &str) -> (String, String) {
63        (name.to_owned(), value.to_owned())
64    }
65
66    #[test]
67    fn no_query_and_an_empty_one_are_no_parameters() {
68        assert_eq!(
69            pairs(None, NAMES),
70            Ok(BTreeMap::new()),
71            "no query string has no parameters"
72        );
73        assert_eq!(
74            read(""),
75            Ok(vec![]),
76            "an empty query string has none either"
77        );
78    }
79
80    #[test]
81    fn known_names_are_read_with_their_values_decoded() {
82        assert_eq!(
83            read("limit=10&since=2026-10-25T12%3A00%3A00%2B01%3A00"),
84            Ok(vec![
85                pair("limit", "10"),
86                pair("since", "2026-10-25T12:00:00+01:00")
87            ]),
88            "each value is percent-decoded"
89        );
90        assert_eq!(
91            read("cursor="),
92            Ok(vec![pair("cursor", "")]),
93            "a name with an empty value is given, with that value"
94        );
95    }
96
97    #[test]
98    fn an_unknown_name_is_refused() {
99        assert_eq!(
100            read("limit=10&offset=5"),
101            Err(BadQuery),
102            "offset is not a name this query takes"
103        );
104        assert_eq!(read("Limit=10"), Err(BadQuery), "names are exact");
105    }
106
107    #[test]
108    fn a_name_given_twice_is_refused() {
109        assert_eq!(
110            read("limit=10&limit=20"),
111            Err(BadQuery),
112            "two values would be ambiguous"
113        );
114        assert_eq!(
115            read("limit=10&limit=10"),
116            Err(BadQuery),
117            "a repeated name is refused even with the same value"
118        );
119    }
120
121    #[test]
122    fn a_name_with_no_equals_sign_is_refused() {
123        assert_eq!(
124            read("limit"),
125            Err(BadQuery),
126            "a parameter is a name, an equals sign and a value"
127        );
128    }
129}