1use std::collections::BTreeMap;
6use std::fmt;
7
8#[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
24pub fn pairs(query: Option<&str>, names: &[&str]) -> Result<BTreeMap<String, String>, BadQuery> {
33 let mut read = BTreeMap::new();
34 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}