Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 }) => {
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 {
name: (*param_name).to_string(),
});
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", "")], ""))
);
}

#[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