Skip to main content

gpui_navigator/
params.rs

1//! Route parameter extraction and query string parsing
2//!
3//! This module provides types for working with URL parameters extracted from route
4//! patterns (like `:id`) and query strings (like `?page=1&sort=name`).
5
6use std::collections::HashMap;
7
8/// Route parameters extracted from path segments
9///
10/// # Example
11///
12/// ```
13/// use gpui_navigator::RouteParams;
14///
15/// // Route pattern: /users/:id
16/// // Matched path: /users/123
17/// let mut params = RouteParams::new();
18/// params.insert("id".to_string(), "123".to_string());
19///
20/// assert_eq!(params.get("id"), Some(&"123".to_string()));
21/// assert_eq!(params.get_as::<i32>("id"), Some(123));
22/// ```
23#[derive(Debug, Clone, Default)]
24pub struct RouteParams {
25    params: HashMap<String, String>,
26}
27
28impl RouteParams {
29    /// Create new empty route params
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Create from hashmap
35    pub fn from_map(params: HashMap<String, String>) -> Self {
36        Self { params }
37    }
38
39    /// Get a parameter value as a string
40    pub fn get(&self, key: &str) -> Option<&String> {
41        self.params.get(key)
42    }
43
44    /// Get a parameter and parse it as a specific type
45    ///
46    /// Returns `None` if the parameter doesn't exist or cannot be parsed.
47    pub fn get_as<T>(&self, key: &str) -> Option<T>
48    where
49        T: std::str::FromStr,
50    {
51        self.params.get(key)?.parse().ok()
52    }
53
54    /// Insert a parameter
55    pub fn insert(&mut self, key: String, value: String) {
56        self.params.insert(key, value);
57    }
58
59    /// Set a parameter (alias for insert)
60    pub fn set(&mut self, key: String, value: String) {
61        self.params.insert(key, value);
62    }
63
64    /// Check if parameter exists
65    pub fn contains(&self, key: &str) -> bool {
66        self.params.contains_key(key)
67    }
68
69    /// Get all parameters as a reference to the HashMap
70    pub fn all(&self) -> &HashMap<String, String> {
71        &self.params
72    }
73
74    /// Get mutable reference to parameters HashMap
75    pub fn all_mut(&mut self) -> &mut HashMap<String, String> {
76        &mut self.params
77    }
78
79    /// Iterate over all parameters
80    pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
81        self.params.iter()
82    }
83
84    /// Check if parameters are empty
85    pub fn is_empty(&self) -> bool {
86        self.params.is_empty()
87    }
88
89    /// Get number of parameters
90    pub fn len(&self) -> usize {
91        self.params.len()
92    }
93}
94
95// ============================================================================
96// Tests
97// ============================================================================
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    // Route parameters tests
104
105    #[test]
106    fn test_route_params_basic() {
107        let mut params = RouteParams::new();
108        params.insert("id".to_string(), "123".to_string());
109
110        assert_eq!(params.get("id"), Some(&"123".to_string()));
111        assert!(params.contains("id"));
112        assert!(!params.contains("missing"));
113    }
114
115    #[test]
116    fn test_route_params_get_as() {
117        let mut params = RouteParams::new();
118        params.insert("id".to_string(), "123".to_string());
119        params.insert("active".to_string(), "true".to_string());
120
121        assert_eq!(params.get_as::<i32>("id"), Some(123));
122        assert_eq!(params.get_as::<u32>("id"), Some(123));
123        assert_eq!(params.get_as::<bool>("active"), Some(true));
124        assert_eq!(params.get_as::<i32>("missing"), None);
125    }
126
127    #[test]
128    fn test_route_params_from_map() {
129        let mut map = HashMap::new();
130        map.insert("name".to_string(), "John".to_string());
131        map.insert("age".to_string(), "30".to_string());
132
133        let params = RouteParams::from_map(map);
134
135        assert_eq!(params.get("name"), Some(&"John".to_string()));
136        assert_eq!(params.get_as::<i32>("age"), Some(30));
137    }
138
139    #[test]
140    fn test_route_params_set() {
141        let mut params = RouteParams::new();
142        params.set("key".to_string(), "value".to_string());
143
144        assert_eq!(params.get("key"), Some(&"value".to_string()));
145    }
146
147    #[test]
148    fn test_route_params_all() {
149        let mut params = RouteParams::new();
150        params.insert("a".to_string(), "1".to_string());
151        params.insert("b".to_string(), "2".to_string());
152
153        let all = params.all();
154        assert_eq!(all.len(), 2);
155        assert_eq!(all.get("a"), Some(&"1".to_string()));
156    }
157
158    #[test]
159    fn test_route_params_iter() {
160        let mut params = RouteParams::new();
161        params.insert("x".to_string(), "1".to_string());
162        params.insert("y".to_string(), "2".to_string());
163
164        let count = params.iter().count();
165        assert_eq!(count, 2);
166    }
167
168    #[test]
169    fn test_route_params_empty() {
170        let params = RouteParams::new();
171        assert!(params.is_empty());
172        assert_eq!(params.len(), 0);
173
174        let mut params = RouteParams::new();
175        params.insert("key".to_string(), "value".to_string());
176        assert!(!params.is_empty());
177        assert_eq!(params.len(), 1);
178    }
179}
180
181// ============================================================================
182// Query Parameters
183// ============================================================================
184
185/// Query parameters parsed from URL query string
186///
187/// Supports multiple values for the same key.
188///
189/// # Example
190///
191/// ```
192/// use gpui_navigator::QueryParams;
193///
194/// let query = QueryParams::from_query_string("page=1&sort=name&tag=rust&tag=gpui");
195///
196/// assert_eq!(query.get("page"), Some(&"1".to_string()));
197/// assert_eq!(query.get_as::<i32>("page"), Some(1));
198/// assert_eq!(query.get_all("tag").unwrap().len(), 2);
199/// ```
200#[derive(Debug, Clone, Default)]
201pub struct QueryParams {
202    params: HashMap<String, Vec<String>>,
203}
204
205impl QueryParams {
206    /// Create new empty query params
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Parse from query string
212    ///
213    /// # Example
214    ///
215    /// ```
216    /// use gpui_navigator::QueryParams;
217    ///
218    /// let query = QueryParams::from_query_string("page=1&sort=name");
219    /// assert_eq!(query.get("page"), Some(&"1".to_string()));
220    /// ```
221    pub fn from_query_string(query: &str) -> Self {
222        let mut params = HashMap::new();
223
224        for pair in query.split('&') {
225            if let Some((key, value)) = pair.split_once('=') {
226                // Simple URL decoding (replace %20 with space, etc.)
227                let key = decode_uri_component(key);
228                let value = decode_uri_component(value);
229
230                params.entry(key).or_insert_with(Vec::new).push(value);
231            }
232        }
233
234        Self { params }
235    }
236
237    /// Get first value for a parameter
238    pub fn get(&self, key: &str) -> Option<&String> {
239        self.params.get(key)?.first()
240    }
241
242    /// Get all values for a parameter
243    ///
244    /// Useful for parameters that can appear multiple times like `?tag=rust&tag=gpui`
245    pub fn get_all(&self, key: &str) -> Option<&Vec<String>> {
246        self.params.get(key)
247    }
248
249    /// Get parameter as a specific type
250    ///
251    /// Returns the first value parsed as type T.
252    pub fn get_as<T>(&self, key: &str) -> Option<T>
253    where
254        T: std::str::FromStr,
255    {
256        self.get(key)?.parse().ok()
257    }
258
259    /// Insert a parameter
260    ///
261    /// If the key already exists, the value is appended to the list.
262    pub fn insert(&mut self, key: String, value: String) {
263        self.params.entry(key).or_default().push(value);
264    }
265
266    /// Check if parameter exists
267    pub fn contains(&self, key: &str) -> bool {
268        self.params.contains_key(key)
269    }
270
271    /// Convert to query string
272    ///
273    /// # Example
274    ///
275    /// ```
276    /// use gpui_navigator::QueryParams;
277    ///
278    /// let mut query = QueryParams::new();
279    /// query.insert("page".to_string(), "1".to_string());
280    /// let s = query.to_query_string();
281    /// assert!(s.contains("page=1"));
282    /// ```
283    pub fn to_query_string(&self) -> String {
284        let pairs: Vec<String> = self
285            .params
286            .iter()
287            .flat_map(|(key, values)| {
288                values.iter().map(move |value| {
289                    format!(
290                        "{}={}",
291                        encode_uri_component(key),
292                        encode_uri_component(value)
293                    )
294                })
295            })
296            .collect();
297
298        pairs.join("&")
299    }
300
301    /// Check if parameters are empty
302    pub fn is_empty(&self) -> bool {
303        self.params.is_empty()
304    }
305
306    /// Get number of unique parameter keys
307    pub fn len(&self) -> usize {
308        self.params.len()
309    }
310}
311
312/// Simple URI component encoding (encode special characters)
313fn encode_uri_component(s: &str) -> String {
314    s.chars()
315        .map(|c| match c {
316            'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
317            ' ' => "%20".to_string(),
318            _ => format!("%{:02X}", c as u8),
319        })
320        .collect()
321}
322
323/// Simple URI component decoding
324fn decode_uri_component(s: &str) -> String {
325    let mut result = String::new();
326    let mut chars = s.chars().peekable();
327
328    while let Some(c) = chars.next() {
329        if c == '%' {
330            // Try to decode hex pair
331            let hex: String = chars.by_ref().take(2).collect();
332            if let Ok(byte) = u8::from_str_radix(&hex, 16) {
333                result.push(byte as char);
334            } else {
335                result.push('%');
336                result.push_str(&hex);
337            }
338        } else if c == '+' {
339            result.push(' ');
340        } else {
341            result.push(c);
342        }
343    }
344
345    result
346}
347
348// Query parameters tests
349
350#[test]
351fn test_query_params_basic() {
352    let query = QueryParams::from_query_string("page=1&sort=name&filter=active");
353
354    assert_eq!(query.get("page"), Some(&"1".to_string()));
355    assert_eq!(query.get("sort"), Some(&"name".to_string()));
356    assert_eq!(query.get("filter"), Some(&"active".to_string()));
357    assert_eq!(query.get("missing"), None);
358}
359
360#[test]
361fn test_query_params_get_as() {
362    let query = QueryParams::from_query_string("page=1&limit=50&active=true");
363
364    assert_eq!(query.get_as::<i32>("page"), Some(1));
365    assert_eq!(query.get_as::<usize>("limit"), Some(50));
366    assert_eq!(query.get_as::<bool>("active"), Some(true));
367    assert_eq!(query.get_as::<i32>("missing"), None);
368}
369
370#[test]
371fn test_query_params_multiple_values() {
372    let query = QueryParams::from_query_string("tag=rust&tag=gpui&tag=ui");
373
374    let tags = query.get_all("tag").unwrap();
375    assert_eq!(tags.len(), 3);
376    assert!(tags.contains(&"rust".to_string()));
377    assert!(tags.contains(&"gpui".to_string()));
378    assert!(tags.contains(&"ui".to_string()));
379
380    // get() returns first value
381    assert_eq!(query.get("tag"), Some(&"rust".to_string()));
382}
383
384#[test]
385fn test_query_params_insert() {
386    let mut query = QueryParams::new();
387    query.insert("key".to_string(), "value1".to_string());
388    query.insert("key".to_string(), "value2".to_string());
389
390    let values = query.get_all("key").unwrap();
391    assert_eq!(values.len(), 2);
392    assert_eq!(values[0], "value1");
393    assert_eq!(values[1], "value2");
394}
395
396#[test]
397fn test_uri_encoding() {
398    let encoded = encode_uri_component("hello world");
399    assert_eq!(encoded, "hello%20world");
400
401    let encoded = encode_uri_component("test@example.com");
402    assert!(encoded.contains("%40")); // @ encoded as %40
403}
404
405#[test]
406fn test_uri_decoding() {
407    let decoded = decode_uri_component("hello%20world");
408    assert_eq!(decoded, "hello world");
409
410    let decoded = decode_uri_component("hello+world");
411    assert_eq!(decoded, "hello world");
412}
413
414#[test]
415fn test_to_query_string() {
416    let mut query = QueryParams::new();
417    query.insert("page".to_string(), "1".to_string());
418    query.insert("sort".to_string(), "name".to_string());
419
420    let s = query.to_query_string();
421    // Order may vary, check both keys are present
422    assert!(s.contains("page=1"));
423    assert!(s.contains("sort=name"));
424}
425
426#[test]
427fn test_query_params_empty() {
428    let query = QueryParams::new();
429    assert!(query.is_empty());
430    assert_eq!(query.len(), 0);
431
432    let mut query = QueryParams::new();
433    query.insert("key".to_string(), "value".to_string());
434    assert!(!query.is_empty());
435    assert_eq!(query.len(), 1);
436}
437
438#[test]
439fn test_empty_query_string() {
440    let query = QueryParams::from_query_string("");
441    assert!(query.is_empty());
442}