cardinal_plugins/utils/
mod.rs

1use std::collections::HashMap;
2
3/// Parse query string into a `HashMap<String, Vec<String>>`
4/// Keeps all values when a key appears multiple times.
5pub fn parse_query_string_multi(qs: &str) -> HashMap<String, Vec<String>> {
6    let mut map: HashMap<String, Vec<String>> = HashMap::new();
7
8    for (key, value) in form_urlencoded::parse(qs.as_bytes()).into_owned() {
9        map.entry(key).or_default().push(value);
10    }
11
12    map
13}
14
15#[cfg(test)]
16mod tests {
17    use super::parse_query_string_multi;
18
19    #[test]
20    fn empty_string_returns_empty_map() {
21        let m = parse_query_string_multi("");
22        assert!(m.is_empty());
23    }
24
25    #[test]
26    fn single_pair_parses_correctly() {
27        let m = parse_query_string_multi("user=a");
28        assert_eq!(m.len(), 1);
29        assert_eq!(m.get("user"), Some(&vec!["a".to_string()]));
30    }
31
32    #[test]
33    fn repeated_keys_accumulate_in_order() {
34        let m = parse_query_string_multi("id=1&id=2&id=3");
35        assert_eq!(m.len(), 1);
36        assert_eq!(
37            m.get("id"),
38            Some(&vec!["1".to_string(), "2".to_string(), "3".to_string()])
39        );
40    }
41
42    #[test]
43    fn multiple_distinct_keys() {
44        let m = parse_query_string_multi("user=a&role=admin&user=b");
45        assert_eq!(m.len(), 2);
46        assert_eq!(m.get("role"), Some(&vec!["admin".to_string()]));
47        assert_eq!(m.get("user"), Some(&vec!["a".to_string(), "b".to_string()]));
48    }
49
50    #[test]
51    fn percent_decoding_and_plus_as_space() {
52        // url::form_urlencoded decodes %XX and treats '+' as space.
53        let m = parse_query_string_multi("name=Andr%C3%A9s+Pirela&k%2Bey=v%2Balue");
54        assert_eq!(m.get("name"), Some(&vec!["Andrés Pirela".to_string()]));
55        // %2B becomes '+', not space.
56        assert_eq!(m.get("k+ey"), Some(&vec!["v+alue".to_string()]));
57    }
58
59    #[test]
60    fn empty_value_is_kept_as_empty_string() {
61        let m = parse_query_string_multi("a=&b=");
62        assert_eq!(m.get("a"), Some(&vec!["".to_string()]));
63        assert_eq!(m.get("b"), Some(&vec!["".to_string()]));
64    }
65
66    #[test]
67    fn bare_key_gets_empty_string_value() {
68        // "flag" without '=' should parse as ("flag", "")
69        let m = parse_query_string_multi("flag&x=1");
70        assert_eq!(m.get("flag"), Some(&vec!["".to_string()]));
71        assert_eq!(m.get("x"), Some(&vec!["1".to_string()]));
72    }
73
74    #[test]
75    fn unicode_keys_and_values() {
76        let m = parse_query_string_multi("名=値&emoji=%F0%9F%98%80"); // 😀
77        assert_eq!(m.get("名"), Some(&vec!["値".to_string()]));
78        assert_eq!(m.get("emoji"), Some(&vec!["😀".to_string()]));
79    }
80
81    #[test]
82    fn preserves_insertion_order_within_value_vectors() {
83        // Ensure the per-key Vec preserves the order the pairs appear in the string.
84        let m = parse_query_string_multi("k=first&x=1&k=second&k=third&x=2");
85        assert_eq!(
86            m.get("k"),
87            Some(&vec![
88                "first".to_string(),
89                "second".to_string(),
90                "third".to_string()
91            ])
92        );
93        assert_eq!(m.get("x"), Some(&vec!["1".to_string(), "2".to_string()]));
94    }
95
96    #[test]
97    fn mixed_empty_and_nonempty_values() {
98        let m = parse_query_string_multi("k=&k=1&&k=2"); // note the empty pair between &&
99        assert_eq!(
100            m.get("k"),
101            Some(&vec!["".to_string(), "1".to_string(), "2".to_string()])
102        );
103    }
104
105    #[test]
106    fn plus_in_key_and_value_becomes_space() {
107        let m = parse_query_string_multi("a+b=c+d&a+b=e+f");
108        assert_eq!(
109            m.get("a b"),
110            Some(&vec!["c d".to_string(), "e f".to_string()])
111        );
112    }
113}