Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c6666c2
feat(router): add wildcard path matching support
dharshan-0 Jun 2, 2026
22d88ac
test(router): add tests for wildcard path matching
dharshan-0 Jun 2, 2026
30250f1
fix(router): fixed formatting
dharshan-0 Jun 2, 2026
d4d8c39
docs(introduction.md): document wildcard path matching
dharshan-0 Jun 3, 2026
8d0ab73
fix(router): fixed clippy errors and warnings
dharshan-0 Jun 3, 2026
7378518
fix(router): fixed formatting
dharshan-0 Jun 3, 2026
f247cc7
Merge branch 'master' into wildcard-feature
dharshan-0 Jun 3, 2026
0dc4687
test(router): add test for wildcard param
dharshan-0 Jun 3, 2026
4788c9d
fix(router): change wildcard param syntax
dharshan-0 Jun 9, 2026
22fbeed
test(router): change test for new path syntax
dharshan-0 Jun 9, 2026
62bb573
Merge branch 'master' into wildcard-feature
dharshan-0 Jun 10, 2026
669fac5
docs(introduction.md): update wildcard params to new syntax
dharshan-0 Jun 10, 2026
26cba98
Merge remote-tracking branch 'refs/remotes/origin/wildcard-feature' i…
dharshan-0 Jun 10, 2026
4559b71
docs(introduction.md): fix wildcard routing code example
dharshan-0 Jun 11, 2026
0f45536
docs(introduction.md): fix missed name fn in wildcard routing code ex…
dharshan-0 Jun 11, 2026
83a3ca0
fix(router): improve error message for invalid wildcard parameter names
dharshan-0 Jun 17, 2026
2179b38
Merge branch 'master' into wildcard-feature
ElijahAhianyo Jun 18, 2026
89fac85
Merge branch 'master' into wildcard-feature
ElijahAhianyo Jun 22, 2026
ccd7901
test(router): add test for illegal wildcard
dharshan-0 Jun 28, 2026
77ea6ca
feat(router): add precedence based routing
dharshan-0 Jun 28, 2026
66adf0c
test(router.rs): add test for precedence based routing
dharshan-0 Jun 28, 2026
e854310
docs(introduction.md): add documentation for route precedence
dharshan-0 Jun 28, 2026
8389365
Merge branch 'master' into wildcard-feature
dharshan-0 Jun 28, 2026
0e6c9ef
fix(router): made clippy happy
dharshan-0 Jun 30, 2026
44d1185
Merge branch 'master' into wildcard-feature
dharshan-0 Jul 6, 2026
dd863b8
fix(router.rs): fixed typo in the test
dharshan-0 Jul 7, 2026
997251d
Merge branch 'master' into wildcard-feature
dharshan-0 Jul 7, 2026
1ef194b
Merge branch 'master' into wildcard-feature
dharshan-0 Jul 12, 2026
1ee0097
Merge branch 'master' into wildcard-feature
dharshan-0 Jul 14, 2026
fae85a9
Merge branch 'master' into wildcard-feature
dharshan-0 Jul 17, 2026
938647a
Merge branch 'master' into wildcard-feature
dharshan-0 Jul 21, 2026
71e3b98
Merge branch 'master' into wildcard-feature
dharshan-0 Jul 27, 2026
c20c4d8
Merge branch 'master' into wildcard-feature
dharshan-0 Aug 2, 2026
b95caec
Merge branch 'master' into wildcard-feature
dharshan-0 Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions cot/src/router/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ impl PathMatcher {
(Some('/') | None, State::Param { start }) => {
panic!("Unclosed parameter: `{}`", &path_pattern[start..index]);
}
(Some('*'), State::Literal { start }) => {
Comment thread
ElijahAhianyo marked this conversation as resolved.
Outdated
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();

assert!(
next_char.is_some(),
"Wildcard must be named: `{path_pattern}`"
);
assert!(
!param_name.contains('/'),
"Wildcard must be last: `{param_name}`"
);
assert!(
Self::is_param_name_valid(&param_name[1..]),
"Invalid parameter name: `{param_name}`"
);

parts.push(PathPart::Literal(literal_name.to_string()));
parts.push(PathPart::Param {
Comment thread
ElijahAhianyo marked this conversation as resolved.
Outdated
name: (*param_name).to_string(),
Comment thread
ElijahAhianyo marked this conversation as resolved.
Outdated
});
break;
}
_ => {}
}
}
Expand Down Expand Up @@ -126,6 +150,10 @@ impl PathMatcher {
}
current_path = &current_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 {
Expand Down Expand Up @@ -526,4 +554,92 @@ mod tests {
let params = ReverseParamMap::new();
assert_eq!(path_parser.reverse(&params).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", "")], ""))
Comment thread
ElijahAhianyo marked this conversation as resolved.
Outdated
);
}

#[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/*");
}

#[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");
}
}
19 changes: 19 additions & 0 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Html> {
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
Expand Down
Loading