Skip to main content

gpui_navigator/
params.rs

1//! Route parameter extraction and query string parsing.
2//!
3//! This module provides two complementary types for working with URL data:
4//!
5//! - [`RouteParams`] — path parameters extracted from dynamic segments (e.g.
6//!   `:id` in `/users/:id`). Supports typed access via [`get_as`](RouteParams::get_as),
7//!   parent-child merging via [`merge`](RouteParams::merge), and extraction from
8//!   raw paths via [`from_path`](RouteParams::from_path).
9//! - [`QueryParams`] — query string parameters parsed from the `?key=value&...`
10//!   portion of a URL. Supports multi-valued keys (e.g. `?tag=a&tag=b`), typed
11//!   access, and round-trip serialization.
12//!
13//! # Example
14//!
15//! ```
16//! use gpui_navigator::{RouteParams, QueryParams};
17//!
18//! // Path parameters from /users/42
19//! let mut params = RouteParams::new();
20//! params.set("id".to_string(), "42".to_string());
21//! assert_eq!(params.get_as::<u32>("id"), Some(42));
22//!
23//! // Query parameters from ?page=1&sort=name
24//! let query = QueryParams::from_query_string("page=1&sort=name");
25//! assert_eq!(query.get_as::<u32>("page"), Some(1));
26//! assert_eq!(query.get("sort"), Some(&"name".to_string()));
27//! ```
28
29use std::collections::HashMap;
30
31/// Route parameters extracted from path segments
32///
33/// # Example
34///
35/// ```
36/// use gpui_navigator::RouteParams;
37///
38/// // Route pattern: /users/:id
39/// // Matched path: /users/123
40/// let mut params = RouteParams::new();
41/// params.insert("id".to_string(), "123".to_string());
42///
43/// assert_eq!(params.get("id"), Some(&"123".to_string()));
44/// assert_eq!(params.get_as::<i32>("id"), Some(123));
45/// ```
46#[derive(Debug, Clone, Default, PartialEq, Eq)]
47pub struct RouteParams {
48    params: HashMap<String, String>,
49}
50
51impl RouteParams {
52    /// Create empty route parameters.
53    #[inline]
54    #[must_use] 
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Create from an existing `HashMap`.
60    #[must_use] 
61    pub const fn from_map(params: HashMap<String, String>) -> Self {
62        Self { params }
63    }
64
65    /// Get a parameter value by key.
66    #[must_use] 
67    pub fn get(&self, key: &str) -> Option<&String> {
68        self.params.get(key)
69    }
70
71    /// Get a parameter and parse it as a specific type
72    ///
73    /// Returns `None` if the parameter doesn't exist or cannot be parsed.
74    #[must_use] 
75    pub fn get_as<T>(&self, key: &str) -> Option<T>
76    where
77        T: std::str::FromStr,
78    {
79        self.params.get(key)?.parse().ok()
80    }
81
82    /// Insert or overwrite a parameter.
83    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
84        self.params.insert(key.into(), value.into());
85    }
86
87    /// Set a parameter (alias for [`insert`](Self::insert)).
88    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
89        self.params.insert(key.into(), value.into());
90    }
91
92    /// Return `true` if the given key is present.
93    #[must_use] 
94    pub fn contains(&self, key: &str) -> bool {
95        self.params.contains_key(key)
96    }
97
98    /// Get a reference to the underlying parameter map.
99    #[must_use] 
100    pub const fn all(&self) -> &HashMap<String, String> {
101        &self.params
102    }
103
104    /// Get a mutable reference to the underlying parameter map.
105    pub fn all_mut(&mut self) -> &mut HashMap<String, String> {
106        &mut self.params
107    }
108
109    /// Iterate over all `(key, value)` pairs.
110    pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
111        self.params.iter()
112    }
113
114    /// Return `true` if there are no parameters.
115    #[must_use] 
116    pub fn is_empty(&self) -> bool {
117        self.params.is_empty()
118    }
119
120    /// Return the number of parameters.
121    #[must_use] 
122    pub fn len(&self) -> usize {
123        self.params.len()
124    }
125
126    /// Merge parent parameters with child parameters
127    ///
128    /// Child parameters override parent parameters in case of collision.
129    /// This is used for nested routing to inherit parent route parameters.
130    ///
131    /// # Example
132    ///
133    /// ```
134    /// use gpui_navigator::RouteParams;
135    ///
136    /// let mut parent = RouteParams::new();
137    /// parent.set("workspaceId".to_string(), "123".to_string());
138    /// parent.set("view".to_string(), "list".to_string());
139    ///
140    /// let mut child = RouteParams::new();
141    /// child.set("projectId".to_string(), "456".to_string());
142    /// child.set("view".to_string(), "grid".to_string()); // Override parent
143    ///
144    /// let merged = RouteParams::merge(&parent, &child);
145    /// assert_eq!(merged.get("workspaceId"), Some(&"123".to_string()));
146    /// assert_eq!(merged.get("projectId"), Some(&"456".to_string()));
147    /// assert_eq!(merged.get("view"), Some(&"grid".to_string())); // Child wins
148    /// ```
149    #[must_use] 
150    pub fn merge(parent: &Self, child: &Self) -> Self {
151        let mut merged = parent.clone();
152        merged
153            .params
154            .extend(child.params.iter().map(|(k, v)| (k.clone(), v.clone())));
155        merged
156    }
157
158    /// Extract route parameters from a path given a pattern
159    ///
160    /// T045: Helper function for User Story 5 - Parameter Inheritance.
161    /// Matches a path against a pattern and extracts parameter values.
162    ///
163    /// # Pattern Syntax
164    ///
165    /// - `:paramName` - Dynamic segment that matches any value
166    /// - `literal` - Static segment that must match exactly
167    ///
168    /// # Example
169    ///
170    /// ```
171    /// use gpui_navigator::RouteParams;
172    ///
173    /// // Pattern: /users/:userId/posts/:postId
174    /// // Path: /users/123/posts/456
175    /// let params = RouteParams::from_path("/users/123/posts/456", "/users/:userId/posts/:postId");
176    ///
177    /// assert_eq!(params.get("userId"), Some(&"123".to_string()));
178    /// assert_eq!(params.get("postId"), Some(&"456".to_string()));
179    ///
180    /// // No match returns empty params
181    /// let params = RouteParams::from_path("/products/xyz", "/users/:userId");
182    /// assert!(params.is_empty());
183    /// ```
184    #[must_use] 
185    pub fn from_path(path: &str, pattern: &str) -> Self {
186        let mut params = Self::new();
187
188        // Split path and pattern into segments
189        let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
190        let pattern_segments: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
191
192        // Must have same number of segments
193        if path_segments.len() != pattern_segments.len() {
194            return params;
195        }
196
197        // Match each segment
198        for (path_seg, pattern_seg) in path_segments.iter().zip(pattern_segments.iter()) {
199            if let Some(param_name) = pattern_seg.strip_prefix(':') {
200                // Dynamic segment - extract parameter
201                // Handle type constraints like :id<i32> -> extract "id"
202                let param_name = param_name
203                    .find('<')
204                    .map_or(param_name, |pos| &param_name[..pos]);
205                params.insert(param_name.to_string(), (*path_seg).to_string());
206            } else if pattern_seg != path_seg {
207                // Static segment mismatch - no match
208                return Self::new();
209            }
210        }
211
212        params
213    }
214}
215
216// ============================================================================
217// Tests
218// ============================================================================
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    // Route parameters tests
225
226    #[test]
227    fn test_route_params_basic() {
228        let mut params = RouteParams::new();
229        params.insert("id".to_string(), "123".to_string());
230
231        assert_eq!(params.get("id"), Some(&"123".to_string()));
232        assert!(params.contains("id"));
233        assert!(!params.contains("missing"));
234    }
235
236    #[test]
237    fn test_route_params_get_as() {
238        let mut params = RouteParams::new();
239        params.insert("id".to_string(), "123".to_string());
240        params.insert("active".to_string(), "true".to_string());
241
242        assert_eq!(params.get_as::<i32>("id"), Some(123));
243        assert_eq!(params.get_as::<u32>("id"), Some(123));
244        assert_eq!(params.get_as::<bool>("active"), Some(true));
245        assert_eq!(params.get_as::<i32>("missing"), None);
246    }
247
248    #[test]
249    fn test_route_params_from_map() {
250        let mut map = HashMap::new();
251        map.insert("name".to_string(), "John".to_string());
252        map.insert("age".to_string(), "30".to_string());
253
254        let params = RouteParams::from_map(map);
255
256        assert_eq!(params.get("name"), Some(&"John".to_string()));
257        assert_eq!(params.get_as::<i32>("age"), Some(30));
258    }
259
260    #[test]
261    fn test_route_params_set() {
262        let mut params = RouteParams::new();
263        params.set("key".to_string(), "value".to_string());
264
265        assert_eq!(params.get("key"), Some(&"value".to_string()));
266    }
267
268    #[test]
269    fn test_route_params_all() {
270        let mut params = RouteParams::new();
271        params.insert("a".to_string(), "1".to_string());
272        params.insert("b".to_string(), "2".to_string());
273
274        let all = params.all();
275        assert_eq!(all.len(), 2);
276        assert_eq!(all.get("a"), Some(&"1".to_string()));
277    }
278
279    #[test]
280    fn test_route_params_iter() {
281        let mut params = RouteParams::new();
282        params.insert("x".to_string(), "1".to_string());
283        params.insert("y".to_string(), "2".to_string());
284
285        let count = params.iter().count();
286        assert_eq!(count, 2);
287    }
288
289    #[test]
290    fn test_route_params_empty() {
291        let params = RouteParams::new();
292        assert!(params.is_empty());
293        assert_eq!(params.len(), 0);
294
295        let mut params = RouteParams::new();
296        params.insert("key".to_string(), "value".to_string());
297        assert!(!params.is_empty());
298        assert_eq!(params.len(), 1);
299    }
300}
301
302// ============================================================================
303// Query Parameters
304// ============================================================================
305
306/// Query parameters parsed from URL query string
307///
308/// Supports multiple values for the same key.
309///
310/// # Example
311///
312/// ```
313/// use gpui_navigator::QueryParams;
314///
315/// let query = QueryParams::from_query_string("page=1&sort=name&tag=rust&tag=gpui");
316///
317/// assert_eq!(query.get("page"), Some(&"1".to_string()));
318/// assert_eq!(query.get_as::<i32>("page"), Some(1));
319/// assert_eq!(query.get_all("tag").unwrap().len(), 2);
320/// ```
321#[derive(Debug, Clone, Default, PartialEq, Eq)]
322pub struct QueryParams {
323    params: HashMap<String, Vec<String>>,
324}
325
326impl QueryParams {
327    /// Create empty query parameters.
328    #[inline]
329    #[must_use] 
330    pub fn new() -> Self {
331        Self::default()
332    }
333
334    /// Parse from query string
335    ///
336    /// # Example
337    ///
338    /// ```
339    /// use gpui_navigator::QueryParams;
340    ///
341    /// let query = QueryParams::from_query_string("page=1&sort=name");
342    /// assert_eq!(query.get("page"), Some(&"1".to_string()));
343    /// ```
344    pub fn from_query_string(query: &str) -> Self {
345        let mut params = HashMap::new();
346
347        for pair in query.split('&') {
348            if let Some((key, value)) = pair.split_once('=') {
349                // Simple URL decoding (replace %20 with space, etc.)
350                let key = decode_uri_component(key);
351                let value = decode_uri_component(value);
352
353                params.entry(key).or_insert_with(Vec::new).push(value);
354            }
355        }
356
357        Self { params }
358    }
359
360    /// Get the first value for a key.
361    #[must_use] 
362    pub fn get(&self, key: &str) -> Option<&String> {
363        self.params.get(key)?.first()
364    }
365
366    /// Get all values for a key.
367    ///
368    /// Useful for parameters that can appear multiple times (e.g. `?tag=rust&tag=gpui`).
369    #[must_use] 
370    pub fn get_all(&self, key: &str) -> Option<&Vec<String>> {
371        self.params.get(key)
372    }
373
374    /// Get the first value for a key, parsed as type `T`.
375    ///
376    /// Returns `None` if the key is missing or the value cannot be parsed.
377    #[must_use] 
378    pub fn get_as<T>(&self, key: &str) -> Option<T>
379    where
380        T: std::str::FromStr,
381    {
382        self.get(key)?.parse().ok()
383    }
384
385    /// Append a value for the given key.
386    ///
387    /// If the key already exists, the new value is added to the list (not replaced).
388    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
389        self.params
390            .entry(key.into())
391            .or_default()
392            .push(value.into());
393    }
394
395    /// Return `true` if the given key is present.
396    #[must_use] 
397    pub fn contains(&self, key: &str) -> bool {
398        self.params.contains_key(key)
399    }
400
401    /// Serialize back into a query string.
402    ///
403    /// # Example
404    ///
405    /// ```
406    /// use gpui_navigator::QueryParams;
407    ///
408    /// let mut query = QueryParams::new();
409    /// query.insert("page".to_string(), "1".to_string());
410    /// let s = query.to_query_string();
411    /// assert!(s.contains("page=1"));
412    /// ```
413    #[must_use] 
414    pub fn to_query_string(&self) -> String {
415        let pairs: Vec<String> = self
416            .params
417            .iter()
418            .flat_map(|(key, values)| {
419                values.iter().map(move |value| {
420                    format!(
421                        "{}={}",
422                        encode_uri_component(key),
423                        encode_uri_component(value)
424                    )
425                })
426            })
427            .collect();
428
429        pairs.join("&")
430    }
431
432    /// Return `true` if there are no parameters.
433    #[must_use] 
434    pub fn is_empty(&self) -> bool {
435        self.params.is_empty()
436    }
437
438    /// Return the number of unique parameter keys.
439    #[must_use] 
440    pub fn len(&self) -> usize {
441        self.params.len()
442    }
443}
444
445/// Simple URI component encoding (encode special characters)
446///
447/// Encodes non-unreserved characters as percent-encoded UTF-8 bytes,
448/// correctly handling multi-byte Unicode characters.
449fn encode_uri_component(s: &str) -> String {
450    use std::fmt::Write;
451    let mut result = String::with_capacity(s.len());
452    for c in s.chars() {
453        match c {
454            'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
455            _ => {
456                let mut buf = [0u8; 4];
457                for byte in c.encode_utf8(&mut buf).bytes() {
458                    let _ = write!(result, "%{byte:02X}");
459                }
460            }
461        }
462    }
463    result
464}
465
466/// Simple URI component decoding
467///
468/// Decodes percent-encoded UTF-8 byte sequences back into characters.
469/// Also handles `+` as space (form encoding).
470fn decode_uri_component(s: &str) -> String {
471    let mut bytes = Vec::with_capacity(s.len());
472    let mut chars = s.chars();
473
474    while let Some(c) = chars.next() {
475        if c == '%' {
476            let hex: String = chars.by_ref().take(2).collect();
477            if let Ok(byte) = u8::from_str_radix(&hex, 16) {
478                bytes.push(byte);
479            } else {
480                bytes.push(b'%');
481                bytes.extend_from_slice(hex.as_bytes());
482            }
483        } else if c == '+' {
484            bytes.push(b' ');
485        } else {
486            let mut buf = [0u8; 4];
487            let encoded = c.encode_utf8(&mut buf);
488            bytes.extend_from_slice(encoded.as_bytes());
489        }
490    }
491
492    String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
493}
494
495// Query parameters tests
496
497#[test]
498fn test_query_params_basic() {
499    let query = QueryParams::from_query_string("page=1&sort=name&filter=active");
500
501    assert_eq!(query.get("page"), Some(&"1".to_string()));
502    assert_eq!(query.get("sort"), Some(&"name".to_string()));
503    assert_eq!(query.get("filter"), Some(&"active".to_string()));
504    assert_eq!(query.get("missing"), None);
505}
506
507#[test]
508fn test_query_params_get_as() {
509    let query = QueryParams::from_query_string("page=1&limit=50&active=true");
510
511    assert_eq!(query.get_as::<i32>("page"), Some(1));
512    assert_eq!(query.get_as::<usize>("limit"), Some(50));
513    assert_eq!(query.get_as::<bool>("active"), Some(true));
514    assert_eq!(query.get_as::<i32>("missing"), None);
515}
516
517#[test]
518fn test_query_params_multiple_values() {
519    let query = QueryParams::from_query_string("tag=rust&tag=gpui&tag=ui");
520
521    let tags = query.get_all("tag").unwrap();
522    assert_eq!(tags.len(), 3);
523    assert!(tags.contains(&"rust".to_string()));
524    assert!(tags.contains(&"gpui".to_string()));
525    assert!(tags.contains(&"ui".to_string()));
526
527    // get() returns first value
528    assert_eq!(query.get("tag"), Some(&"rust".to_string()));
529}
530
531#[test]
532fn test_query_params_insert() {
533    let mut query = QueryParams::new();
534    query.insert("key".to_string(), "value1".to_string());
535    query.insert("key".to_string(), "value2".to_string());
536
537    let values = query.get_all("key").unwrap();
538    assert_eq!(values.len(), 2);
539    assert_eq!(values[0], "value1");
540    assert_eq!(values[1], "value2");
541}
542
543#[test]
544fn test_uri_encoding() {
545    let encoded = encode_uri_component("hello world");
546    assert_eq!(encoded, "hello%20world");
547
548    let encoded = encode_uri_component("test@example.com");
549    assert!(encoded.contains("%40")); // @ encoded as %40
550}
551
552#[test]
553fn test_uri_decoding() {
554    let decoded = decode_uri_component("hello%20world");
555    assert_eq!(decoded, "hello world");
556
557    let decoded = decode_uri_component("hello+world");
558    assert_eq!(decoded, "hello world");
559}
560
561#[test]
562fn test_to_query_string() {
563    let mut query = QueryParams::new();
564    query.insert("page".to_string(), "1".to_string());
565    query.insert("sort".to_string(), "name".to_string());
566
567    let s = query.to_query_string();
568    // Order may vary, check both keys are present
569    assert!(s.contains("page=1"));
570    assert!(s.contains("sort=name"));
571}
572
573#[test]
574fn test_query_params_empty() {
575    let query = QueryParams::new();
576    assert!(query.is_empty());
577    assert_eq!(query.len(), 0);
578
579    let mut query = QueryParams::new();
580    query.insert("key".to_string(), "value".to_string());
581    assert!(!query.is_empty());
582    assert_eq!(query.len(), 1);
583}
584
585#[test]
586fn test_empty_query_string() {
587    let query = QueryParams::from_query_string("");
588    assert!(query.is_empty());
589}