torrust_tracker/servers/http/v1/
query.rs

1//! The `Query` struct used to parse and store the URL query parameters.
2//!
3/// ```text
4/// URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
5/// ```
6use std::panic::Location;
7use std::str::FromStr;
8
9use multimap::MultiMap;
10use thiserror::Error;
11
12type ParamName = String;
13type ParamValue = String;
14
15/// It represents a URL query component.
16///
17/// ```text
18/// URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
19/// ```
20#[derive(Debug)]
21pub struct Query {
22    /* code-review:
23        - Consider using a third-party crate.
24        - Conversion from/to string is not deterministic. Params can be in a different order in the query string.
25    */
26    params: MultiMap<ParamName, NameValuePair>,
27}
28
29impl Query {
30    /// It return `Some(value)` for a URL query param if the param with the
31    /// input `name` exists. For example:
32    ///
33    /// ```rust
34    /// use torrust_tracker::servers::http::v1::query::Query;
35    ///
36    /// let raw_query = "param1=value1&param2=value2";
37    ///
38    /// let query = raw_query.parse::<Query>().unwrap();
39    ///
40    /// assert_eq!(query.get_param("param1").unwrap(), "value1");
41    /// assert_eq!(query.get_param("param2").unwrap(), "value2");
42    /// ```
43    ///
44    /// It returns only the first param value even if it has multiple values:
45    ///
46    /// ```rust
47    /// use torrust_tracker::servers::http::v1::query::Query;
48    ///
49    /// let raw_query = "param1=value1&param1=value2";
50    ///
51    /// let query = raw_query.parse::<Query>().unwrap();
52    ///
53    /// assert_eq!(query.get_param("param1").unwrap(), "value1");
54    /// ```
55    #[must_use]
56    pub fn get_param(&self, name: &str) -> Option<String> {
57        self.params.get(name).map(|pair| pair.value.clone())
58    }
59
60    /// Returns all the param values as a vector.
61    ///
62    /// ```rust
63    /// use torrust_tracker::servers::http::v1::query::Query;
64    ///
65    /// let query = "param1=value1&param1=value2".parse::<Query>().unwrap();
66    ///
67    /// assert_eq!(
68    ///     query.get_param_vec("param1"),
69    ///     Some(vec!["value1".to_string(), "value2".to_string()])
70    /// );
71    /// ```
72    ///
73    /// Returns all the param values as a vector even if it has only one value.
74    ///
75    /// ```rust
76    /// use torrust_tracker::servers::http::v1::query::Query;
77    ///
78    /// let query = "param1=value1".parse::<Query>().unwrap();
79    ///
80    /// assert_eq!(
81    ///     query.get_param_vec("param1"), Some(vec!["value1".to_string()])
82    /// );
83    /// ```
84    #[must_use]
85    pub fn get_param_vec(&self, name: &str) -> Option<Vec<String>> {
86        self.params.get_vec(name).map(|pairs| {
87            let mut param_values = vec![];
88            for pair in pairs {
89                param_values.push(pair.value.to_string());
90            }
91            param_values
92        })
93    }
94}
95
96/// This error can be returned when parsing a [`Query`]
97/// from a string.
98#[derive(Error, Debug)]
99pub enum ParseQueryError {
100    /// Invalid URL query param. For example: `"name=value=value"`. It contains
101    /// an unescaped `=` character.
102    #[error("invalid param {raw_param} in {location}")]
103    InvalidParam {
104        location: &'static Location<'static>,
105        raw_param: String,
106    },
107}
108
109impl FromStr for Query {
110    type Err = ParseQueryError;
111
112    fn from_str(raw_query: &str) -> Result<Self, Self::Err> {
113        let mut params: MultiMap<ParamName, NameValuePair> = MultiMap::new();
114
115        let raw_params = raw_query.trim().trim_start_matches('?').split('&').collect::<Vec<&str>>();
116
117        for raw_param in raw_params {
118            let pair: NameValuePair = raw_param.parse()?;
119            let param_name = pair.name.clone();
120            params.insert(param_name, pair);
121        }
122
123        Ok(Self { params })
124    }
125}
126
127impl From<Vec<(&str, &str)>> for Query {
128    fn from(raw_params: Vec<(&str, &str)>) -> Self {
129        let mut params: MultiMap<ParamName, NameValuePair> = MultiMap::new();
130
131        for raw_param in raw_params {
132            params.insert(raw_param.0.to_owned(), NameValuePair::new(raw_param.0, raw_param.1));
133        }
134
135        Self { params }
136    }
137}
138
139impl std::fmt::Display for Query {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        let query = self
142            .params
143            .iter_all()
144            .map(|param| format!("{}", FieldValuePairSet::from_vec(param.1)))
145            .collect::<Vec<String>>()
146            .join("&");
147
148        write!(f, "{query}")
149    }
150}
151
152#[derive(Debug, PartialEq, Clone)]
153struct NameValuePair {
154    name: ParamName,
155    value: ParamValue,
156}
157
158impl NameValuePair {
159    pub fn new(name: &str, value: &str) -> Self {
160        Self {
161            name: name.to_owned(),
162            value: value.to_owned(),
163        }
164    }
165}
166
167impl FromStr for NameValuePair {
168    type Err = ParseQueryError;
169
170    fn from_str(raw_param: &str) -> Result<Self, Self::Err> {
171        let pair = raw_param.split('=').collect::<Vec<&str>>();
172
173        if pair.len() != 2 {
174            return Err(ParseQueryError::InvalidParam {
175                location: Location::caller(),
176                raw_param: raw_param.to_owned(),
177            });
178        }
179
180        Ok(Self {
181            name: pair[0].to_owned(),
182            value: pair[1].to_owned(),
183        })
184    }
185}
186
187impl std::fmt::Display for NameValuePair {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        write!(f, "{}={}", self.name, self.value)
190    }
191}
192
193#[derive(Debug, PartialEq)]
194struct FieldValuePairSet {
195    pairs: Vec<NameValuePair>,
196}
197
198impl FieldValuePairSet {
199    fn from_vec(pair_vec: &Vec<NameValuePair>) -> Self {
200        let mut pairs: Vec<NameValuePair> = vec![];
201
202        for pair in pair_vec {
203            pairs.push(pair.clone());
204        }
205
206        Self { pairs }
207    }
208}
209
210impl std::fmt::Display for FieldValuePairSet {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        let query = self
213            .pairs
214            .iter()
215            .map(|pair| format!("{pair}"))
216            .collect::<Vec<String>>()
217            .join("&");
218
219        write!(f, "{query}")
220    }
221}
222
223#[cfg(test)]
224mod tests {
225
226    mod url_query {
227        use crate::servers::http::v1::query::Query;
228
229        #[test]
230        fn should_parse_the_query_params_from_an_url_query_string() {
231            let raw_query =
232                "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-qB00000000000000001&port=17548";
233
234            let query = raw_query.parse::<Query>().unwrap();
235
236            assert_eq!(
237                query.get_param("info_hash").unwrap(),
238                "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"
239            );
240            assert_eq!(query.get_param("peer_id").unwrap(), "-qB00000000000000001");
241            assert_eq!(query.get_param("port").unwrap(), "17548");
242        }
243
244        #[test]
245        fn should_be_instantiated_from_a_string_pair_vector() {
246            let query = Query::from(vec![("param1", "value1"), ("param2", "value2")]);
247
248            assert_eq!(query.get_param("param1"), Some("value1".to_string()));
249            assert_eq!(query.get_param("param2"), Some("value2".to_string()));
250        }
251
252        #[test]
253        fn should_fail_parsing_an_invalid_query_string() {
254            let invalid_raw_query = "name=value=value";
255
256            let query = invalid_raw_query.parse::<Query>();
257
258            assert!(query.is_err());
259        }
260
261        #[test]
262        fn should_ignore_the_preceding_question_mark_if_it_exists() {
263            let raw_query = "?name=value";
264
265            let query = raw_query.parse::<Query>().unwrap();
266
267            assert_eq!(query.get_param("name"), Some("value".to_string()));
268        }
269
270        #[test]
271        fn should_trim_whitespaces() {
272            let raw_query = " name=value ";
273
274            let query = raw_query.parse::<Query>().unwrap();
275
276            assert_eq!(query.get_param("name"), Some("value".to_string()));
277        }
278
279        mod should_allow_more_than_one_value_for_the_same_param {
280            use crate::servers::http::v1::query::Query;
281
282            #[test]
283            fn instantiated_from_a_vector() {
284                let query1 = Query::from(vec![("param1", "value1"), ("param1", "value2")]);
285                assert_eq!(
286                    query1.get_param_vec("param1"),
287                    Some(vec!["value1".to_string(), "value2".to_string()])
288                );
289            }
290
291            #[test]
292            fn parsed_from_an_string() {
293                let query2 = "param1=value1&param1=value2".parse::<Query>().unwrap();
294                assert_eq!(
295                    query2.get_param_vec("param1"),
296                    Some(vec!["value1".to_string(), "value2".to_string()])
297                );
298            }
299        }
300
301        mod should_be_displayed {
302            use crate::servers::http::v1::query::Query;
303
304            #[test]
305            fn with_one_param() {
306                assert_eq!("param1=value1".parse::<Query>().unwrap().to_string(), "param1=value1");
307            }
308
309            #[test]
310            fn with_multiple_params() {
311                let query = "param1=value1&param2=value2".parse::<Query>().unwrap().to_string();
312                assert!(query == "param1=value1&param2=value2" || query == "param2=value2&param1=value1");
313            }
314
315            #[test]
316            fn with_multiple_values_for_the_same_param() {
317                let query = "param1=value1&param1=value2".parse::<Query>().unwrap().to_string();
318                assert!(query == "param1=value1&param1=value2" || query == "param1=value2&param1=value1");
319            }
320        }
321
322        mod param_name_value_pair {
323            use crate::servers::http::v1::query::NameValuePair;
324
325            #[test]
326            fn should_parse_a_single_query_param() {
327                let raw_param = "name=value";
328
329                let param = raw_param.parse::<NameValuePair>().unwrap();
330
331                assert_eq!(
332                    param,
333                    NameValuePair {
334                        name: "name".to_string(),
335                        value: "value".to_string(),
336                    }
337                );
338            }
339
340            #[test]
341            fn should_fail_parsing_an_invalid_query_param() {
342                let invalid_raw_param = "name=value=value";
343
344                let query = invalid_raw_param.parse::<NameValuePair>();
345
346                assert!(query.is_err());
347            }
348
349            #[test]
350            fn should_be_displayed() {
351                assert_eq!("name=value".parse::<NameValuePair>().unwrap().to_string(), "name=value");
352            }
353        }
354    }
355}