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;
6
7/// A query the route does not take: a name it does not know, a name given
8/// twice, or a value it cannot read.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct BadQuery;
11
12/// The parameters of `query`, percent-decoded, by name. A name that is not
13/// in `names`, or that is given twice, is refused, so a misspelt or
14/// repeated parameter never goes unnoticed. No query is no parameters.
15pub fn pairs(query: Option<&str>, names: &[&str]) -> Result<BTreeMap<String, String>, BadQuery> {
16    let mut read = BTreeMap::new();
17    // Empty pieces, as a trailing `&` leaves, name nothing.
18    for piece in query.unwrap_or("").split('&').filter(|p| !p.is_empty()) {
19        if !piece.contains('=') {
20            return Err(BadQuery);
21        }
22        let (name, value) = form_urlencoded::parse(piece.as_bytes())
23            .next()
24            .ok_or(BadQuery)?;
25        if !names.contains(&name.as_ref()) {
26            return Err(BadQuery);
27        }
28        if read.insert(name.into_owned(), value.into_owned()).is_some() {
29            return Err(BadQuery);
30        }
31    }
32    Ok(read)
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    const NAMES: &[&str] = &["limit", "cursor", "since"];
40
41    fn read(query: &str) -> Result<Vec<(String, String)>, BadQuery> {
42        pairs(Some(query), NAMES).map(|p| p.into_iter().collect())
43    }
44
45    fn pair(name: &str, value: &str) -> (String, String) {
46        (name.to_owned(), value.to_owned())
47    }
48
49    #[test]
50    fn no_query_and_an_empty_one_are_no_parameters() {
51        assert_eq!(pairs(None, NAMES), Ok(BTreeMap::new()));
52        assert_eq!(read(""), Ok(vec![]));
53    }
54
55    #[test]
56    fn known_names_are_read_with_their_values_decoded() {
57        assert_eq!(
58            read("limit=10&since=2026-10-25T12%3A00%3A00%2B01%3A00"),
59            Ok(vec![
60                pair("limit", "10"),
61                pair("since", "2026-10-25T12:00:00+01:00")
62            ])
63        );
64        assert_eq!(read("cursor="), Ok(vec![pair("cursor", "")]));
65    }
66
67    #[test]
68    fn an_unknown_name_is_refused() {
69        assert_eq!(read("limit=10&offset=5"), Err(BadQuery));
70        assert_eq!(read("Limit=10"), Err(BadQuery), "names are exact");
71    }
72
73    #[test]
74    fn a_name_given_twice_is_refused() {
75        assert_eq!(read("limit=10&limit=20"), Err(BadQuery));
76        assert_eq!(read("limit=10&limit=10"), Err(BadQuery));
77    }
78
79    #[test]
80    fn a_name_with_no_equals_sign_is_refused() {
81        assert_eq!(read("limit"), Err(BadQuery));
82    }
83}