urlable/
query.rs

1use crate::encoding::{decode_query_key, decode_query_value, encode_query_key, encode_query_value};
2use indexmap::IndexMap;
3use serde_json::Value;
4
5pub type QueryValue = Value;
6pub type QueryObject = IndexMap<String, QueryValue>;
7
8/// Parses and decodes a query string into an object.
9/// The input can be a query string with or without the leading `?`.
10///
11/// This function handles:
12/// - Query strings with/without leading ?
13/// - Multiple values for same key (joined with commas)
14/// - URL encoded keys and values
15/// - Empty values
16/// - Malformed query strings
17///
18/// # Arguments
19/// * `parameters_string` - The query string to parse (e.g. "foo=bar&baz=qux" or "?foo=bar")
20///
21/// # Returns
22/// * `T` - A IndexMap containing the parsed query parameters where T implements From<IndexMap>
23///
24pub fn parse_query(raw_query: &str) -> QueryObject {
25    let mut object = QueryObject::new();
26    // Strip leading ? if present
27    let params_pair = raw_query.strip_prefix('?').unwrap_or(raw_query);
28
29    // Split on & to get key=value pairs
30    for parameter in params_pair.split('&') {
31        if let Some((key, value)) = parameter.split_once('=') {
32            let key = decode_query_key(key);
33            let value = decode_query_value(value);
34
35            // Handle multiple values for same key by joining with commas
36            match object.get_mut(&key) {
37                None => {
38                    object.insert(key, Value::String(value));
39                }
40                Some(existing) => {
41                    let existing_str = existing.as_str().unwrap_or("");
42                    *existing = Value::String(format!("{},{}", existing_str, value));
43                }
44            }
45        }
46    }
47    object
48}
49
50/// Encodes a key-value pair into a URL query string parameter.
51///
52/// This function handles:
53/// - Null/boolean/number values (key only)
54/// - Array values (multiple key=value pairs)
55/// - String values (key=value)
56/// - URL encoding of keys and values
57///
58/// # Arguments
59/// * `key` - The parameter key to encode
60/// * `value` - The parameter value to encode (as JSON Value)
61///
62/// # Returns
63/// * `String` - The encoded query string parameter
64pub fn encode_query_item(key: &str, value: &QueryValue) -> String {
65    // Handle null/boolean/number values - output key only
66    if value.is_null() || value.is_boolean() || value.is_number() {
67        return encode_query_key(key);
68    }
69
70    // Handle array values - output multiple key=value pairs
71    if let Some(arr) = value.as_array() {
72        return arr
73            .iter()
74            .map(|v| {
75                format!(
76                    "{}={}",
77                    encode_query_key(key),
78                    encode_query_value(v.as_str().unwrap_or(""))
79                )
80            })
81            .collect::<Vec<String>>()
82            .join("&");
83    }
84
85    // Handle string values - output key=value
86    format!(
87        "{}={}",
88        encode_query_key(key),
89        encode_query_value(value.as_str().unwrap_or(""))
90    )
91}
92
93/// Converts a query object into an encoded query string.
94///
95/// This function:
96/// - Filters out null values
97/// - Encodes each key-value pair
98/// - Joins pairs with &
99/// - Handles arrays and complex values
100///
101/// # Arguments
102/// * `query` - The query object to stringify (IndexMap of key-value pairs)
103///
104/// # Returns
105/// * `String` - The stringified and encoded query string
106pub fn stringify_query(query: &QueryObject) -> String {
107    query
108        .iter()
109        .filter(|(_, v)| !v.is_null())
110        .map(|(k, v)| encode_query_item(k, v))
111        .filter(|s| !s.is_empty())
112        .collect::<Vec<String>>()
113        .join("&")
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use serde_json::json;
120
121    #[test]
122    fn test_parse_query_basic() {
123        let query = parse_query("foo=bar&baz=qux");
124        assert_eq!(query.get("foo").unwrap(), "bar");
125        assert_eq!(query.get("baz").unwrap(), "qux");
126    }
127
128    #[test]
129    fn test_parse_query_with_question_mark() {
130        let query = parse_query("?foo=bar&baz=qux");
131        assert_eq!(query.get("foo").unwrap(), "bar");
132        assert_eq!(query.get("baz").unwrap(), "qux");
133    }
134
135    #[test]
136    fn test_parse_query_empty() {
137        let query = parse_query("");
138        assert!(query.is_empty());
139    }
140
141    #[test]
142    fn test_parse_query_encoded() {
143        let query = parse_query("user=john%20doe&email=test%40example.com");
144        assert_eq!(query.get("user").unwrap(), "john doe");
145        assert_eq!(query.get("email").unwrap(), "test@example.com");
146    }
147
148    #[test]
149    fn test_parse_query_multiple_values() {
150        let query = parse_query("color=red&color=blue&color=green");
151        assert_eq!(query.get("color").unwrap(), "red,blue,green");
152    }
153
154    #[test]
155    fn test_encode_query_item_basic() {
156        let key = "test";
157        let value = json!("hello world");
158        assert_eq!(encode_query_item(key, &value), "test=hello+world");
159    }
160
161    #[test]
162    fn test_encode_query_item_array() {
163        let key = "test";
164        let array_value = json!(["a", "b", "c"]);
165        assert_eq!(encode_query_item(key, &array_value), "test=a&test=b&test=c");
166    }
167
168    #[test]
169    fn test_encode_query_item_special_chars() {
170        let key = "email";
171        let value = json!("test@example.com");
172        assert_eq!(encode_query_item(key, &value), "email=test%40example.com");
173    }
174
175    #[test]
176    fn test_encode_query_item_null() {
177        let key = "empty";
178        let value = json!(null);
179        assert_eq!(encode_query_item(key, &value), "empty");
180    }
181
182    #[test]
183    fn test_stringify_query_basic() {
184        let mut query = QueryObject::new();
185        query.insert("foo".to_string(), json!("bar"));
186        query.insert("baz".to_string(), json!("qux"));
187
188        let result = stringify_query(&query);
189        assert!(result.contains("foo=bar"));
190        assert!(result.contains("baz=qux"));
191    }
192
193    #[test]
194    fn test_stringify_query_complex() {
195        let mut query = QueryObject::new();
196        query.insert("user".to_string(), json!("john doe"));
197        query.insert("tags".to_string(), json!(["rust", "coding"]));
198        query.insert("active".to_string(), json!(true));
199        query.insert("empty".to_string(), json!(null));
200
201        let result = stringify_query(&query);
202        assert!(result.contains("user=john+doe"));
203        assert!(result.contains("tags=rust&tags=coding"));
204        assert!(result.contains("active"));
205        assert!(!result.contains("empty"));
206    }
207
208    #[test]
209    fn test_encode_query_item_emoji() {
210        let key = "message";
211        let value = json!("Hello 👋 World 🌍");
212        assert_eq!(
213            encode_query_item(key, &value),
214            "message=Hello+%F0%9F%91%8B+World+%F0%9F%8C%8D"
215        );
216
217        let key = "reaction";
218        let value = json!("❤️");
219        assert_eq!(
220            encode_query_item(key, &value),
221            "reaction=%E2%9D%A4%EF%B8%8F"
222        );
223
224        let key = "faces";
225        let value = json!(["😀", "😎", "🤔"]);
226        assert_eq!(
227            encode_query_item(key, &value),
228            "faces=%F0%9F%98%80&faces=%F0%9F%98%8E&faces=%F0%9F%A4%94"
229        );
230    }
231
232    #[test]
233    fn test_decode_query_item_emoji() {
234        let encoded = "http://example.com?message=Hello+%F0%9F%91%8B+World+%F0%9F%8C%8D";
235        let url = url::Url::parse(encoded).unwrap();
236        let pairs: Vec<(String, String)> = url.query_pairs().into_owned().collect();
237        assert_eq!(
238            pairs[0],
239            ("message".to_string(), "Hello 👋 World 🌍".to_string())
240        );
241
242        let encoded = "http://example.com?reaction=%E2%9D%A4%EF%B8%8F";
243        let url = url::Url::parse(encoded).unwrap();
244        let pairs: Vec<(String, String)> = url.query_pairs().into_owned().collect();
245        assert_eq!(pairs[0], ("reaction".to_string(), "❤️".to_string()));
246
247        let encoded = "http://example.com?faces=%F0%9F%98%80";
248        let url = url::Url::parse(encoded).unwrap();
249        let pairs: Vec<(String, String)> = url.query_pairs().into_owned().collect();
250        assert_eq!(pairs[0], ("faces".to_string(), "😀".to_string()));
251    }
252}