Skip to content

Commit

Permalink
Merge branch 'master' into clippy-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
m4tx authored Jan 30, 2025
2 parents 3199d1a + fa96e52 commit c5bea66
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 31 deletions.
2 changes: 1 addition & 1 deletion cot/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ impl CotApp for AdminApp {
Router::with_urls([
crate::Route::with_handler_and_name("/", index, "index"),
crate::Route::with_handler_and_name("/login", login, "login"),
crate::Route::with_handler_and_name("/:model_name", view_model, "view_model"),
crate::Route::with_handler_and_name("/{model_name}", view_model, "view_model"),
])
}

Expand Down
6 changes: 3 additions & 3 deletions cot/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ mod tests {

#[test]
fn router_reverse_with_param() {
let route = Route::with_handler_and_name("/test/:id", MockHandler, "test");
let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test");
let router = Router::with_urls(vec![route.clone()]);
let mut params = ReverseParamMap::new();
params.insert("id", "123");
Expand Down Expand Up @@ -719,8 +719,8 @@ mod tests {

#[test]
fn route_with_handler_and_params() {
let route = Route::with_handler("/test/:id", MockHandler);
assert_eq!(route.url.to_string(), "/test/:id");
let route = Route::with_handler("/test/{id}", MockHandler);
assert_eq!(route.url.to_string(), "/test/{id}");
}

#[test]
Expand Down
145 changes: 120 additions & 25 deletions cot/src/router/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,66 @@ impl PathMatcher {
let mut parts = Vec::new();
let mut state = State::Literal { start: 0 };

for (index, ch) in path_pattern.chars().map(Some).chain([None]).enumerate() {
let mut char_iter = path_pattern
.chars()
.map(Some)
.chain([None])
.enumerate()
.peekable();
loop {
let Some((index, ch)) = char_iter.next() else {
break;
};

match (ch, state) {
(Some('/') | None, State::Param { start }) => {
let param_name = &path_pattern[start..index];
(Some('{') | None, State::Literal { start }) => {
let literal = &path_pattern[start..index];
if literal.is_empty() {
assert!(
index == 0 || ch.is_none(),
"Consecutive parameters are not allowed"
);
} else {
parts.push(PathPart::Literal(literal.to_string()));
}
state = State::Param { start: index + 1 };
}
(Some('{'), State::Param { start }) => {
if start == index {
// escaped `{`
state = State::Literal { start: index };
} else {
panic!("Unclosed parameter: `{}`", &path_pattern[start..index]);
}
}
(Some('}'), State::Literal { start }) => {
let next_char = char_iter.peek().map(|(_, ch)| *ch).unwrap_or_default();

if next_char == Some('}') {
// escaped `}`
let literal = &path_pattern[start..=index];
parts.push(PathPart::Literal(literal.to_string()));

char_iter.next();
state = State::Literal { start: index + 2 };
} else {
panic!("Closing brace encountered without opening brace");
}
}
(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(),
name: (*param_name).to_string(),
});
state = State::Literal { start: index };
}
(Some(':') | None, State::Literal { start }) => {
let literal = &path_pattern[start..index];
if !literal.is_empty() {
parts.push(PathPart::Literal(literal.to_string()));
}
state = State::Param { start: index + 1 };
state = State::Literal { start: index + 1 };
}
(Some(':'), State::Param { .. }) => {
panic!("Consecutive parameters are not allowed");
(Some('/') | None, State::Param { start }) => {
panic!("Unclosed parameter: `{}`", &path_pattern[start..index]);
}
_ => {}
}
Expand Down Expand Up @@ -264,8 +301,11 @@ enum PathPart {
impl Display for PathPart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathPart::Literal(s) => write!(f, "{s}"),
PathPart::Param { name } => write!(f, ":{name}"),
PathPart::Literal(s) => {
let s = s.replace('{', "{{").replace('}', "}}");
write!(f, "{s}")
}
PathPart::Param { name } => write!(f, "{{{name}}}"),
}
}
}
Expand Down Expand Up @@ -306,9 +346,18 @@ mod tests {
assert_eq!(path_parser.capture("/test"), None);
}

#[test]
fn path_parser_escaped() {
let path_parser = PathMatcher::new("/users/{{{{{{escaped}}}}}}");
assert_eq!(
path_parser.capture("/users/{{{escaped}}}"),
Some(CaptureResult::new(vec![], ""))
);
}

#[test]
fn path_parser_single_param() {
let path_parser = PathMatcher::new("/users/:id");
let path_parser = PathMatcher::new("/users/{id}");
assert_eq!(
path_parser.capture("/users/123"),
Some(CaptureResult::new(vec![PathParam::new("id", "123")], ""))
Expand All @@ -327,9 +376,19 @@ mod tests {
assert_eq!(path_parser.capture("/users/"), None);
}

#[test]
fn path_parser_param_whitespace() {
let path_parser = PathMatcher::new("/users/{ id }");

assert_eq!(
path_parser.capture("/users/123"),
Some(CaptureResult::new(vec![PathParam::new("id", "123")], ""))
);
}

#[test]
fn path_parser_multiple_params() {
let path_parser = PathMatcher::new("/users/:id/posts/:post_id");
let path_parser = PathMatcher::new("/users/{id}/posts/{post_id}");
assert_eq!(
path_parser.capture("/users/123/posts/456"),
Some(CaptureResult::new(
Expand All @@ -355,30 +414,66 @@ mod tests {
#[test]
#[should_panic(expected = "Consecutive parameters are not allowed")]
fn path_parser_consecutive_params() {
let _ = PathMatcher::new("/users/:id:post_id");
let _ = PathMatcher::new("/users/{id}{post_id}");
}

#[test]
#[should_panic(expected = "Invalid parameter name: ``")]
fn path_parser_invalid_name_empty() {
let _ = PathMatcher::new("/users/:");
let _ = PathMatcher::new("/users/{}");
}

#[test]
#[should_panic(expected = "Invalid parameter name: `123`")]
fn path_parser_invalid_name_numeric() {
let _ = PathMatcher::new("/users/:123");
let _ = PathMatcher::new("/users/{123}");
}

#[test]
#[should_panic(expected = "Invalid parameter name: `abc#$%`")]
fn path_parser_invalid_name_non_alphanumeric() {
let _ = PathMatcher::new("/users/:abc#$%");
let _ = PathMatcher::new("/users/{abc#$%}");
}

#[test]
#[should_panic(expected = "Unclosed parameter: `foo`")]
fn path_parser_unclosed() {
let _ = PathMatcher::new("/users/{foo");
}

#[test]
#[should_panic(expected = "Closing brace encountered without opening brace")]
fn path_parser_missing_opening_brace() {
let _ = PathMatcher::new("/users/foo}");
}

#[test]
#[should_panic(expected = "Unclosed parameter: `foo`")]
fn path_parser_unclosed_slash() {
let _ = PathMatcher::new("/users/{foo/bar");
}

#[test]
#[should_panic(expected = "Unclosed parameter: `foo`")]
fn path_parser_unclosed_double() {
let _ = PathMatcher::new("/users/{foo{bar");
}

#[test]
#[should_panic(expected = "Closing brace encountered without opening brace")]
fn path_parser_escaping_unclosed() {
let _ = PathMatcher::new("/users/{{{foo}}/bar");
}

#[test]
fn path_parser_display() {
let path_parser = PathMatcher::new("/users/{id}/posts/{{escaped}}");
assert_eq!(format!("{path_parser}"), "/users/{id}/posts/{{escaped}}");
}

#[test]
fn reverse_with_valid_params() {
let path_parser = PathMatcher::new("/users/:id/posts/:post_id");
let path_parser = PathMatcher::new("/users/{id}/posts/{post_id}");
let mut params = ReverseParamMap::new();
params.insert("id", "123");
params.insert("post_id", "456");
Expand All @@ -390,7 +485,7 @@ mod tests {

#[test]
fn reverse_with_missing_param() {
let path_parser = PathMatcher::new("/users/:id/posts/:post_id");
let path_parser = PathMatcher::new("/users/{id}/posts/{post_id}");
let mut params = ReverseParamMap::new();
params.insert("id", "123");
let result = path_parser.reverse(&params);
Expand All @@ -403,7 +498,7 @@ mod tests {

#[test]
fn reverse_with_extra_param() {
let path_parser = PathMatcher::new("/users/:id/posts/:post_id");
let path_parser = PathMatcher::new("/users/{id}/posts/{post_id}");
let mut params = ReverseParamMap::new();
params.insert("id", "123");
params.insert("post_id", "456");
Expand Down
2 changes: 1 addition & 1 deletion cot/tests/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async fn project() -> CotProject {
fn router(&self) -> Router {
Router::with_urls([
Route::with_handler_and_name("/", index, "index"),
Route::with_handler_and_name("/get/:name", parameterized, "parameterized"),
Route::with_handler_and_name("/get/{name}", parameterized, "parameterized"),
])
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/todo-list/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ impl CotApp for TodoApp {
Router::with_urls([
Route::with_handler_and_name("/", index, "index"),
Route::with_handler_and_name("/todos/add", add_todo, "add-todo"),
Route::with_handler_and_name("/todos/:todo_id/remove", remove_todo, "remove-todo"),
Route::with_handler_and_name("/todos/{todo_id}/remove", remove_todo, "remove-todo"),
])
}
}
Expand Down

0 comments on commit c5bea66

Please sign in to comment.