diff --git a/cot/src/router/path.rs b/cot/src/router/path.rs index ba1f6fd67..7c58e5c57 100644 --- a/cot/src/router/path.rs +++ b/cot/src/router/path.rs @@ -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(¶m_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; + } _ => {} } } @@ -126,6 +150,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 { @@ -526,4 +554,92 @@ 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/*"); + } + + #[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"); + } } diff --git a/docs/introduction.md b/docs/introduction.md index 244d606a1..e0cd56e17 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