1use std::collections::BTreeMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct BadQuery;
11
12pub fn pairs(query: Option<&str>, names: &[&str]) -> Result<BTreeMap<String, String>, BadQuery> {
16 let mut read = BTreeMap::new();
17 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}