From c6666c2da4650803d02d183ee5103703ec96ba1e Mon Sep 17 00:00:00 2001 From: "S.Dharshan" Date: Tue, 2 Jun 2026 22:17:09 +0530 Subject: [PATCH 1/7] feat(router): add wildcard path matching support --- cot/src/router/path.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index ba1f6fd67..877ce347d 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -86,6 +86,22 @@ impl PathMatcher { (Some('/') | None, State::Param { start }) => { panic!("Unclosed parameter: `{}`", &path_pattern[start..index]); } + (Some('*'), State::Literal { start } ) => { + let literal_name = &path_pattern[start..index]; + let param_name = &path_pattern[index..].trim(); + let next_char = char_iter.peek().map(|(_, ch)| *ch).unwrap_or_default(); + if next_char.is_none() { + panic!("Wildcard must be named: `{}`", path_pattern); + } + if param_name.contains("/") { + panic!("Wildcard must be last: `{}`", param_name); + } + parts.push(PathPart::Literal(literal_name.to_string())); + parts.push(PathPart::Param { + name: (*param_name).to_string(), + }); + break; + } _ => {} } } @@ -126,6 +142,10 @@ impl PathMatcher { } current_path = ¤t_path[s.len()..]; } + PathPart::Param { name } if name.starts_with('*') => { + params.push(PathParam::new(name, current_path)); + current_path = ""; + } PathPart::Param { name } => { let next_slash = current_path.find('/'); let value = if let Some(next_slash) = next_slash { From 22d88ac48925216547bbfa9712a2344e3fe2bee7 Mon Sep 17 00:00:00 2001 From: "S.Dharshan" Date: Tue, 2 Jun 2026 22:17:51 +0530 Subject: [PATCH 2/7] test(router): add tests for wildcard path matching --- cot/src/router/path.rs | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index 877ce347d..0192c9d2c 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -546,4 +546,76 @@ mod tests { let params = ReverseParamMap::new(); assert_eq!(path_parser.reverse(¶ms).unwrap(), "/cafĂ©/test"); } + + #[test] + fn path_parser_wildcard_root() { + let path_parser = PathMatcher::new("/*path"); + assert_eq!( + path_parser.capture("/foo/bar"), + Some(CaptureResult::new( + vec![PathParam::new("*path", "foo/bar")], + "" + )) + ); + } + + #[test] + fn path_parser_wildcard_single_segment() { + let path_parser = PathMatcher::new("/users/rand/*path"); + assert_eq!( + path_parser.capture("/users/rand/foo"), + Some(CaptureResult::new( + vec![PathParam::new("*path", "foo")], + "" + )) + ); + } + + #[test] + fn path_parser_wildcard_multi_segment() { + let path_parser = PathMatcher::new("/users/rand/*path"); + assert_eq!( + path_parser.capture("/users/rand/foo/bar"), + Some(CaptureResult::new( + vec![PathParam::new("*path", "foo/bar")], + "" + )) + ); + } + + #[test] + fn path_parser_wildcard_no_match() { + let path_parser = PathMatcher::new("/prefix/*path"); + assert_eq!(path_parser.capture("/other/foo"), None); + } + + #[test] + fn path_parser_wildcard_empty() { + let path_parser = PathMatcher::new("/users/rand/*path"); + assert_eq!( + path_parser.capture("/users/rand/"), + Some(CaptureResult::new(vec![PathParam::new("*path", "")], "")) + ); + } + + #[test] + fn path_parser_wildcard_with_param() { + let path_parser = PathMatcher::new("/users/{id}/*path"); + assert_eq!( + path_parser.capture("/users/42/foo"), + Some(CaptureResult::new(vec![PathParam::new("id", "42"), PathParam::new("*path", "foo")], "")) + ); + } + + #[test] + #[should_panic(expected = "Wildcard must be last: `*path/extra`")] + fn path_parser_wildcard_with_trailing_literal() { + let _ = PathMatcher::new("/*path/extra"); + } + + #[test] + #[should_panic(expected = "Wildcard must be named: `users/rand/*`")] + fn path_parser_with_no_wildcard_name() { + let _ = PathMatcher::new("users/rand/*"); + } } From 30250f122d0860bfaf0d27df78da5dc7b62da294 Mon Sep 17 00:00:00 2001 From: "S.Dharshan" Date: Tue, 2 Jun 2026 22:27:37 +0530 Subject: [PATCH 3/7] fix(router): fixed formatting --- cot/src/router/path.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index 0192c9d2c..dbdbd3c49 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -86,7 +86,7 @@ impl PathMatcher { (Some('/') | None, State::Param { start }) => { panic!("Unclosed parameter: `{}`", &path_pattern[start..index]); } - (Some('*'), State::Literal { start } ) => { + (Some('*'), State::Literal { start }) => { let literal_name = &path_pattern[start..index]; let param_name = &path_pattern[index..].trim(); let next_char = char_iter.peek().map(|(_, ch)| *ch).unwrap_or_default(); @@ -564,10 +564,7 @@ mod tests { let path_parser = PathMatcher::new("/users/rand/*path"); assert_eq!( path_parser.capture("/users/rand/foo"), - Some(CaptureResult::new( - vec![PathParam::new("*path", "foo")], - "" - )) + Some(CaptureResult::new(vec![PathParam::new("*path", "foo")], "")) ); } @@ -603,7 +600,10 @@ mod tests { let path_parser = PathMatcher::new("/users/{id}/*path"); assert_eq!( path_parser.capture("/users/42/foo"), - Some(CaptureResult::new(vec![PathParam::new("id", "42"), PathParam::new("*path", "foo")], "")) + Some(CaptureResult::new( + vec![PathParam::new("id", "42"), PathParam::new("*path", "foo")], + "" + )) ); } From d4d8c3982b9063ca72221710a8a0511ade94b3e3 Mon Sep 17 00:00:00 2001 From: "S.Dharshan" Date: Wed, 3 Jun 2026 11:18:09 +0530 Subject: [PATCH 4/7] docs(introduction.md): document wildcard path matching --- docs/introduction.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/introduction.md b/docs/introduction.md index b2658a61c..d3ced8a3d 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -171,6 +171,25 @@ fn router(&self) -> Router { Now, when you visit [`localhost:8000/hello/John/Smith/`](http://localhost:8000/hello/John), you should see `Hello, John Smith!` displayed on the page! +cot also has wildcards to match all sub path in a segment: + +```rust +async fn wildcard_path(Path(path): Path<(String, String)>) -> cot::Result { + Ok(Html::new(format!("Passed path: {path}"))) +} + +// inside `impl App`: + +fn router(&self) -> Router { + Router::with_urls([ + // ... + Route::with_handler_and_name("/wildcard/*path", wildcard_path, "wildcard_path"), + ]) +} +``` + +In this case, it matches `/wildcard/`, `/wildcard/foo`, `/wildcard/foo/bar` and so on. + ## Project structure ### App From 8d0ab7331c69f106101d8c5a5109e275ad5ba99c Mon Sep 17 00:00:00 2001 From: "S.Dharshan" Date: Wed, 3 Jun 2026 11:37:57 +0530 Subject: [PATCH 5/7] fix(router): fixed clippy errors and warnings --- cot/src/router/path.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index dbdbd3c49..b73b32e20 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -90,12 +90,10 @@ impl PathMatcher { let literal_name = &path_pattern[start..index]; let param_name = &path_pattern[index..].trim(); let next_char = char_iter.peek().map(|(_, ch)| *ch).unwrap_or_default(); - if next_char.is_none() { - panic!("Wildcard must be named: `{}`", path_pattern); - } - if param_name.contains("/") { - panic!("Wildcard must be last: `{}`", param_name); - } + + assert!(next_char.is_some(), "Wildcard must be named: `{path_pattern}`"); + assert!(!param_name.contains('/'), "Wildcard must be last: `{param_name}`"); + parts.push(PathPart::Literal(literal_name.to_string())); parts.push(PathPart::Param { name: (*param_name).to_string(), From 7378518e98654d97b644713321d1ac0e26384fc8 Mon Sep 17 00:00:00 2001 From: "S.Dharshan" Date: Wed, 3 Jun 2026 11:40:27 +0530 Subject: [PATCH 6/7] fix(router): fixed formatting --- cot/src/router/path.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index b73b32e20..f09567859 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -91,8 +91,14 @@ impl PathMatcher { let param_name = &path_pattern[index..].trim(); let next_char = char_iter.peek().map(|(_, ch)| *ch).unwrap_or_default(); - assert!(next_char.is_some(), "Wildcard must be named: `{path_pattern}`"); - assert!(!param_name.contains('/'), "Wildcard must be last: `{param_name}`"); + assert!( + next_char.is_some(), + "Wildcard must be named: `{path_pattern}`" + ); + assert!( + !param_name.contains('/'), + "Wildcard must be last: `{param_name}`" + ); parts.push(PathPart::Literal(literal_name.to_string())); parts.push(PathPart::Param { From 0dc4687204897e5e30b51fac993215e793e5002d Mon Sep 17 00:00:00 2001 From: "S.Dharshan" Date: Wed, 3 Jun 2026 13:47:04 +0530 Subject: [PATCH 7/7] test(router): add test for wildcard param --- cot/src/router/path.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index f09567859..7c58e5c57 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -99,6 +99,10 @@ impl PathMatcher { !param_name.contains('/'), "Wildcard must be last: `{param_name}`" ); + assert!( + Self::is_param_name_valid(¶m_name[1..]), + "Invalid parameter name: `{param_name}`" + ); parts.push(PathPart::Literal(literal_name.to_string())); parts.push(PathPart::Param { @@ -622,4 +626,20 @@ mod tests { fn path_parser_with_no_wildcard_name() { let _ = PathMatcher::new("users/rand/*"); } + + #[test] + fn path_parser_with_wildcard_name_contains_number_and_strings() { + let _ = PathMatcher::new("users/rand/*path123"); + } + + #[test] + fn path_parser_with_wildcard_name_starts_with_underscore() { + let _ = PathMatcher::new("users/rand/*_path"); + } + + #[test] + #[should_panic(expected = "Invalid parameter name: `*42`")] + fn path_parser_with_wildcard_name_starts_with_numbers() { + let _ = PathMatcher::new("users/rand/*42"); + } }