url_search_params/
lib.rs

1//! # url search params
2//!
3//! `url-search-params` provides the ability to create search params from HashMap and vice versa.
4//!
5//! In [url](https://en.wikipedia.org/wiki/URL) (web address) search params correspond to [query string](https://en.wikipedia.org/wiki/Query_string).
6//!
7//! Keep in mind it works with the query string part of the URL, it is not intended to work on the whole URL by design.
8//! As per specification, the question mark `?` URL delimiter is not part of a query string.
9//!
10//! Also hash mark `#` url delimiter and fragment part of URL is not the parts of a query string.
11//! In practice, it means, the fragment and preceding hash mark won't be sent in a request to a server.
12//!
13use std::collections::HashMap;
14
15
16/// Convert given string into a HashMap containing query string parameters as
17/// key-value pairs
18///
19/// # Examples
20///
21/// ```
22///    use std::collections::HashMap;
23///    use url_search_params::parse_url_search_params;
24///
25///    let search_params: &str = "key=value&another_key=its_value";
26///    let params: HashMap<String, String> = parse_url_search_params(search_params);
27///
28///    // validating output
29///    assert_eq!(2, params.len());
30///
31///    let boxed_get = params.get("key");
32///    assert!(boxed_get.is_some());
33///
34///    let actual_param_value = boxed_get.unwrap();
35///    assert_eq!(actual_param_value, "value");
36///
37///    let boxed_get = params.get("another_key");
38///    assert!(boxed_get.is_some());
39///
40///    let actual_param_value = boxed_get.unwrap();
41///    assert_eq!(actual_param_value, "its_value");
42/// ```
43pub fn parse_url_search_params(params: &str) -> HashMap<String, String> {
44    let mut params_map : HashMap<String, String> = HashMap::new();
45
46    if params.trim().is_empty() {
47        return params_map
48    }
49
50    let split_iter = params.split("&").into_iter();
51    for param in split_iter {
52        let mut key = "";
53        let mut value = "";
54
55        let mut key_value = param.split("=").into_iter();
56        let boxed_key = key_value.next();
57        if boxed_key.is_some() {
58            key = boxed_key.unwrap();
59        }
60
61        let boxed_value = key_value.next();
62        if boxed_value.is_some() {
63            value = boxed_value.unwrap();
64        }
65
66        if !key.is_empty() {
67            params_map.insert(decode_uri_component(key), decode_uri_component(value));
68        }
69
70    }
71    params_map
72}
73
74
75/// Convert given HashMap into a query string
76///
77/// # Examples
78///
79/// ```
80///
81/// use std::collections::HashMap;
82/// use url_search_params::{build_url_search_params, parse_url_search_params};
83///
84/// let mut params_map: HashMap<String, String> = HashMap::new();
85/// params_map.insert("key1&".to_string(), "test1=".to_string());
86/// params_map.insert("key2".to_string(), "test2".to_string());
87///
88/// let search_params : String = build_url_search_params(params_map);
89///
90/// // validating output
91/// let parsed_search_params: HashMap<String, String> = parse_url_search_params(&search_params);
92///
93/// let boxed_get = parsed_search_params.get("key1&");
94/// assert!(boxed_get.is_some());
95///
96/// let actual_param_value = boxed_get.unwrap();
97/// assert_eq!(actual_param_value, "test1=");
98///
99/// let boxed_get = parsed_search_params.get("key2");
100/// assert!(boxed_get.is_some());
101///
102/// let actual_param_value = boxed_get.unwrap();
103/// assert_eq!(actual_param_value, "test2");
104///
105///
106/// ```
107pub fn build_url_search_params(params: HashMap<String, String>) -> String {
108
109    let mut key_value_list : Vec<String> = vec![];
110    for (key, value) in params {
111        let param = [encode_uri_component(key.as_str()), "=".to_string(), encode_uri_component(value.as_str())].join("");
112        key_value_list.push(param);
113    }
114
115    key_value_list.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
116    let url_search_params : String = key_value_list.join("&");
117
118    url_search_params
119}
120
121pub fn encode_uri_component(component: &str) -> String {
122    let mut _result = component.replace(SYMBOL.percent, "%25");
123    _result = _result.replace(SYMBOL.whitespace, "%20");
124    _result = _result.replace(SYMBOL.carriage_return, "%0D");
125    _result = _result.replace(SYMBOL.new_line, "%0A");
126    _result = _result.replace(SYMBOL.exclamation_mark, "%21");
127    _result = _result.replace(SYMBOL.quotation_mark, "%22");
128    _result = _result.replace(SYMBOL.number_sign, "%23");
129    _result = _result.replace(SYMBOL.dollar, "%24");
130    _result = _result.replace(SYMBOL.ampersand, "%26");
131    _result = _result.replace(SYMBOL.single_quote, "%27");
132    _result = _result.replace(SYMBOL.opening_bracket, "%28");
133    _result = _result.replace(SYMBOL.closing_bracket, "%29");
134    _result = _result.replace(SYMBOL.asterisk, "%2A");
135    _result = _result.replace(SYMBOL.plus, "%2B");
136    _result = _result.replace(SYMBOL.comma, "%2C");
137    _result = _result.replace(SYMBOL.slash, "%2F");
138    _result = _result.replace(SYMBOL.colon, "%3A");
139    _result = _result.replace(SYMBOL.semicolon, "%3B");
140    _result = _result.replace(SYMBOL.equals, "%3D");
141    _result = _result.replace(SYMBOL.at, "%40");
142    _result = _result.replace(SYMBOL.opening_square_bracket, "%5B");
143    _result = _result.replace(SYMBOL.closing_square_bracket, "%5D");
144
145
146    return _result
147}
148
149pub fn decode_uri_component(component: &str) -> String {
150    let mut _result = component.replace( "%20", SYMBOL.whitespace);
151    _result = _result.replace("%0A", SYMBOL.new_line);
152    _result = _result.replace ("%0D", SYMBOL.carriage_return);
153    _result = _result.replace ("%21", SYMBOL.exclamation_mark);
154    _result = _result.replace ("%22", SYMBOL.quotation_mark);
155    _result = _result.replace ("%23", SYMBOL.number_sign);
156    _result = _result.replace ("%24", SYMBOL.dollar);
157    _result = _result.replace ("%25", SYMBOL.percent);
158    _result = _result.replace ("%26", SYMBOL.ampersand);
159    _result = _result.replace ("%27", SYMBOL.single_quote);
160    _result = _result.replace ("%28", SYMBOL.opening_bracket);
161    _result = _result.replace ("%29", SYMBOL.closing_bracket);
162    _result = _result.replace ("%2A", SYMBOL.asterisk);
163    _result = _result.replace ("%2B", SYMBOL.plus);
164    _result = _result.replace ("%2C", SYMBOL.comma);
165    _result = _result.replace ("%2F", SYMBOL.slash);
166    _result = _result.replace ("%3A", SYMBOL.colon);
167    _result = _result.replace ("%3B", SYMBOL.semicolon);
168    _result = _result.replace ("%3D", SYMBOL.equals);
169    _result = _result.replace ("%3F", SYMBOL.question_mark);
170    _result = _result.replace ("%40", SYMBOL.at);
171    _result = _result.replace ("%5B", SYMBOL.opening_square_bracket);
172    _result = _result.replace ("%5D", SYMBOL.closing_square_bracket);
173
174    return _result
175}
176
177pub struct Symbol {
178    pub new_line_carriage_return: &'static str,
179    pub new_line: &'static str,
180    pub carriage_return: &'static str,
181    pub empty_string: &'static str,
182    pub whitespace: &'static str,
183    pub equals: &'static str,
184    pub comma: &'static str,
185    pub hyphen: &'static str,
186    pub slash: &'static str,
187    pub semicolon: &'static str,
188    pub colon: &'static str,
189    pub number_sign: &'static str,
190    pub opening_square_bracket: &'static str,
191    pub closing_square_bracket: &'static str,
192    pub opening_curly_bracket: &'static str,
193    pub closing_curly_bracket: &'static str,
194    pub quotation_mark: &'static str,
195    pub underscore: &'static str,
196    pub single_quote: &'static str,
197    pub percent: &'static str,
198    pub exclamation_mark: &'static str,
199    pub dollar: &'static str,
200    pub ampersand: &'static str,
201    pub opening_bracket: &'static str,
202    pub closing_bracket: &'static str,
203    pub asterisk: &'static str,
204    pub plus: &'static str,
205    pub question_mark: &'static str,
206    pub at: &'static str,
207}
208
209pub const SYMBOL: Symbol = Symbol {
210    new_line: "\n",
211    carriage_return: "\r",
212    new_line_carriage_return: "\r\n",
213    empty_string: "",
214    whitespace: " ",
215    equals: "=",
216    comma: ",",
217    hyphen: "-",
218    slash: "/",
219    semicolon: ";",
220    colon: ":",
221    number_sign: "#",
222    opening_square_bracket: "[",
223    closing_square_bracket: "]",
224    opening_curly_bracket: "{",
225    closing_curly_bracket: "}",
226    quotation_mark: "\"",
227    underscore: "_",
228    single_quote: "'",
229    percent: "%",
230    exclamation_mark: "!",
231    dollar: "$",
232    ampersand: "&",
233    opening_bracket: "(",
234    closing_bracket: ")",
235    asterisk: "*",
236    plus: "+",
237    question_mark: "?",
238    at: "@",
239};
240
241#[cfg(test)]
242mod tests {
243    use std::collections::HashMap;
244    use crate::{build_url_search_params, decode_uri_component, encode_uri_component, parse_url_search_params};
245
246    #[test]
247    fn build_url_search_params_test() {
248        let mut params_map: HashMap<String, String> = HashMap::new();
249        params_map.insert("key1".to_string(), "test1".to_string());
250        params_map.insert("key2".to_string(), "test2".to_string());
251        params_map.insert("".to_string(), "empty_key".to_string());
252        params_map.insert("empty_value".to_string(), "".to_string());
253
254        let search_params = build_url_search_params(params_map);
255        let parsed_search_params = parse_url_search_params(&search_params);
256
257        let boxed_get = parsed_search_params.get("key1");
258        assert!(boxed_get.is_some());
259
260        let actual_param_value = boxed_get.unwrap();
261        assert_eq!(actual_param_value, "test1");
262
263        let boxed_get = parsed_search_params.get("key2");
264        assert!(boxed_get.is_some());
265
266        let actual_param_value = boxed_get.unwrap();
267        assert_eq!(actual_param_value, "test2");
268
269        let boxed_get = parsed_search_params.get("");
270        assert!(boxed_get.is_none());
271
272        let boxed_get = parsed_search_params.get("empty_value");
273        assert!(boxed_get.is_some());
274
275        let actual_param_value = boxed_get.unwrap();
276        assert_eq!(actual_param_value, "");
277    }
278
279    #[test]
280    fn build_url_search_params_ampersand() {
281        let mut params_map: HashMap<String, String> = HashMap::new();
282        params_map.insert("key1".to_string(), "test1".to_string());
283        params_map.insert("key2".to_string(), "test2".to_string());
284        params_map.insert("".to_string(), "empty_key".to_string());
285        params_map.insert("empty_value".to_string(), "".to_string());
286        params_map.insert("&".to_string(), "unescaped_ampersand_as_key".to_string());
287
288        let search_params = build_url_search_params(params_map);
289        let parsed_search_params = parse_url_search_params(&search_params);
290
291        let boxed_get = parsed_search_params.get("key1");
292        assert!(boxed_get.is_some());
293
294        let actual_param_value = boxed_get.unwrap();
295        assert_eq!(actual_param_value, "test1");
296
297        let boxed_get = parsed_search_params.get("key2");
298        assert!(boxed_get.is_some());
299
300        let actual_param_value = boxed_get.unwrap();
301        assert_eq!(actual_param_value, "test2");
302
303        let boxed_get = parsed_search_params.get("");
304        assert!(boxed_get.is_none());
305
306
307        let boxed_get = parsed_search_params.get("empty_value");
308        assert!(boxed_get.is_some());
309
310        let actual_param_value = boxed_get.unwrap();
311        assert_eq!(actual_param_value, "");
312
313        let boxed_get = parsed_search_params.get("empty_value");
314        assert!(boxed_get.is_some());
315
316    }
317
318    #[test]
319    fn parse_empty_url_search_params() {
320        let search_params = "";
321        let params = parse_url_search_params(search_params);
322        assert_eq!(0, params.len());
323    }
324
325    #[test]
326    fn parse_empty_equals_ampersand_search_params() {
327        let search_params = "=&key2=value2";
328        let params = parse_url_search_params(search_params);
329        assert_eq!(1, params.len());
330
331        let boxed_get = params.get("key2");
332        assert!(boxed_get.is_some());
333
334        let actual_param_value = boxed_get.unwrap();
335        assert_eq!(actual_param_value, "value2");
336    }
337
338    #[test]
339    fn encode_decode() {
340        let component = "\r\n \"%!#$&'()*+,/:;=?@[]][@?=;:/,+*)('&$#!%\" \r\n";
341        let mut _result = encode_uri_component(component);
342        assert_eq!("%0D%0A%20%22%25%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D?%40%5B%5D%5D%5B%40?%3D%3B%3A%2F%2C%2B%2A%29%28%27%26%24%23%21%25%22%20%0D%0A", _result);
343        _result = decode_uri_component(_result.as_str());
344        assert_eq!(component, _result);
345    }
346}