1use std::collections::HashMap;
30
31#[derive(Debug, Clone, Default, PartialEq, Eq)]
47pub struct RouteParams {
48 params: HashMap<String, String>,
49}
50
51impl RouteParams {
52 #[inline]
54 #[must_use]
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 #[must_use]
61 pub const fn from_map(params: HashMap<String, String>) -> Self {
62 Self { params }
63 }
64
65 #[must_use]
67 pub fn get(&self, key: &str) -> Option<&String> {
68 self.params.get(key)
69 }
70
71 #[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 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
84 self.params.insert(key.into(), value.into());
85 }
86
87 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
89 self.params.insert(key.into(), value.into());
90 }
91
92 #[must_use]
94 pub fn contains(&self, key: &str) -> bool {
95 self.params.contains_key(key)
96 }
97
98 #[must_use]
100 pub const fn all(&self) -> &HashMap<String, String> {
101 &self.params
102 }
103
104 pub fn all_mut(&mut self) -> &mut HashMap<String, String> {
106 &mut self.params
107 }
108
109 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
111 self.params.iter()
112 }
113
114 #[must_use]
116 pub fn is_empty(&self) -> bool {
117 self.params.is_empty()
118 }
119
120 #[must_use]
122 pub fn len(&self) -> usize {
123 self.params.len()
124 }
125
126 #[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 #[must_use]
185 pub fn from_path(path: &str, pattern: &str) -> Self {
186 let mut params = Self::new();
187
188 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 if path_segments.len() != pattern_segments.len() {
194 return params;
195 }
196
197 for (path_seg, pattern_seg) in path_segments.iter().zip(pattern_segments.iter()) {
199 if let Some(param_name) = pattern_seg.strip_prefix(':') {
200 let param_name = param_name
203 .find('<')
204 .map_or(param_name, |pos| ¶m_name[..pos]);
205 params.insert(param_name.to_string(), (*path_seg).to_string());
206 } else if pattern_seg != path_seg {
207 return Self::new();
209 }
210 }
211
212 params
213 }
214}
215
216#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[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#[derive(Debug, Clone, Default, PartialEq, Eq)]
322pub struct QueryParams {
323 params: HashMap<String, Vec<String>>,
324}
325
326impl QueryParams {
327 #[inline]
329 #[must_use]
330 pub fn new() -> Self {
331 Self::default()
332 }
333
334 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 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 #[must_use]
362 pub fn get(&self, key: &str) -> Option<&String> {
363 self.params.get(key)?.first()
364 }
365
366 #[must_use]
370 pub fn get_all(&self, key: &str) -> Option<&Vec<String>> {
371 self.params.get(key)
372 }
373
374 #[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 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 #[must_use]
397 pub fn contains(&self, key: &str) -> bool {
398 self.params.contains_key(key)
399 }
400
401 #[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 #[must_use]
434 pub fn is_empty(&self) -> bool {
435 self.params.is_empty()
436 }
437
438 #[must_use]
440 pub fn len(&self) -> usize {
441 self.params.len()
442 }
443}
444
445fn 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
466fn 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#[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 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")); }
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 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}