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
92 changes: 82 additions & 10 deletions cot/src/router/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,32 @@ impl PathMatcher {
}
(Some('}'), State::Param { start }) => {
let param_name = &path_pattern[start..index].trim();
assert!(
Self::is_param_name_valid(param_name),
"Invalid parameter name: `{param_name}`"
);

parts.push(PathPart::Param {
name: (*param_name).to_string(),
});

if let Some(wildcard_name) = param_name.strip_prefix('*') {
assert!(
Self::is_param_name_valid(wildcard_name),
"Invalid wildcard parameter name: `{wildcard_name}`"
);

let next_char = char_iter.peek().map(|(_, ch)| *ch).unwrap_or_default();
assert!(
Comment thread
ElijahAhianyo marked this conversation as resolved.
next_char.is_none(),
"Wildcard must be the last part of the path: `{path_pattern}`"
);

parts.push(PathPart::Wildcard {
name: wildcard_name.to_string(),
});
} else {
assert!(
Self::is_param_name_valid(param_name),
"Invalid parameter name: `{param_name}`"
);

parts.push(PathPart::Param {
name: param_name.to_string(),
});
}
state = State::Literal { start: index + 1 };
}
(Some('/') | None, State::Param { start }) => {
Expand Down Expand Up @@ -126,6 +144,13 @@ impl PathMatcher {
}
current_path = &current_path[s.len()..];
}
PathPart::Wildcard { name } => {
if current_path.is_empty() {
return None;
}
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 All @@ -151,7 +176,7 @@ impl PathMatcher {
for part in &self.parts {
match part {
PathPart::Literal(s) => result.push_str(s),
PathPart::Param { name } => {
PathPart::Param { name } | PathPart::Wildcard { name } => {
let value = params
.get(name)
.ok_or_else(|| ReverseError::MissingParam(name.clone()))?;
Expand All @@ -171,7 +196,7 @@ impl PathMatcher {
pub(super) fn param_names(&self) -> impl Iterator<Item = &str> {
self.parts.iter().filter_map(|part| match part {
PathPart::Literal(..) => None,
PathPart::Param { name } => Some(name.as_str()),
PathPart::Param { name } | PathPart::Wildcard { name } => Some(name.as_str()),
})
}
}
Expand Down Expand Up @@ -299,6 +324,7 @@ impl<'matcher, 'path> CaptureResult<'matcher, 'path> {
enum PathPart {
Literal(String),
Param { name: String },
Wildcard { name: String },
}

impl Display for PathPart {
Expand All @@ -309,6 +335,7 @@ impl Display for PathPart {
write!(f, "{s}")
}
PathPart::Param { name } => write!(f, "{{{name}}}"),
PathPart::Wildcard { name } => write!(f, "{{*{name}}}"),
}
}
}
Expand Down Expand Up @@ -526,4 +553,49 @@ 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_not_allowed() {
let path_parser = PathMatcher::new("/users/rand/{*path}");
assert_eq!(path_parser.capture("/users/rand/"), None);
}
}
25 changes: 25 additions & 0 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,31 @@ 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 supports wildcard parameters to match all sub-paths within a route segment.

```rust
# struct MyApp;
async fn wildcard_path(Path(path): Path<String>) -> cot::Result<Html> {
Ok(Html::new(format!("Passed path: {path}")))
}

// inside `impl App`:
# impl App for MyApp {
fn router(&self) -> Router {
Router::with_urls([
// ...
Route::with_handler_and_name("/wildcard/{*path}", wildcard_path, "wildcard_path"),
])
}
# fn name(&self) -> &str { todo!() }
# }
```

Prefixing the parameter name with an asterisk (`*`) designates it as a wildcard.

In this case, it matches `/wildcard/foo`, `/wildcard/foo/bar` and so on, but it won't match `/wildcard/`.
Also note that, wildcard param can only be used on the end of path pattern.

## Project structure

### App
Expand Down
Loading