-
-
Notifications
You must be signed in to change notification settings - Fork 50
feat: add wildcard path route #586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c6666c2
22d88ac
30250f1
d4d8c39
8d0ab73
7378518
f247cc7
0dc4687
4788c9d
22fbeed
62bb573
669fac5
26cba98
4559b71
0f45536
83a3ca0
2179b38
89fac85
ccd7901
77ea6ca
66adf0c
e854310
8389365
0e6c9ef
44d1185
dd863b8
997251d
1ef194b
1ee0097
fae85a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,14 +76,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!( | ||
| 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 }) => { | ||
|
|
@@ -129,6 +147,13 @@ impl PathMatcher { | |
| } | ||
| current_path = ¤t_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 { | ||
|
|
@@ -154,7 +179,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()))?; | ||
|
|
@@ -174,9 +199,70 @@ 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()), | ||
| }) | ||
| } | ||
|
|
||
| #[expect(clippy::same_item_push)] | ||
| pub(crate) fn token_weights(&self) -> Vec<u8> { | ||
| let mut weights = Vec::with_capacity(self.parts.len()); | ||
| for part in &self.parts { | ||
| match part { | ||
| PathPart::Literal(s) => { | ||
| for _ in 0..s.split('/').filter(|s| !s.is_empty()).count() { | ||
| weights.push(0); | ||
| } | ||
| } | ||
| PathPart::Param { .. } => weights.push(1), | ||
| PathPart::Wildcard { .. } => weights.push(2), | ||
| } | ||
| } | ||
| weights | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn compare_weights(a: &[u8], b: &[u8]) -> std::cmp::Ordering { | ||
| let max_len = std::cmp::max(a.len(), b.len()); | ||
| let mut score: isize = 0; | ||
| let mut lexical_order_a = String::with_capacity(a.len()); | ||
| let mut lexical_order_b = String::with_capacity(b.len()); | ||
|
|
||
| for i in 0..max_len { | ||
| let (wa, wb) = match (a.get(i), b.get(i)) { | ||
| (None, _) => return std::cmp::Ordering::Less, | ||
| (_, None) => return std::cmp::Ordering::Greater, | ||
| (Some(&w), Some(&v)) => (w, v), | ||
| }; | ||
| lexical_order_a.push(char::from_digit(u32::from(wa), 10).unwrap()); | ||
| lexical_order_b.push(char::from_digit(u32::from(wb), 10).unwrap()); | ||
|
|
||
| if wa == 2 && wb == 2 { | ||
| return lexical_order_a.cmp(&lexical_order_b); | ||
| } | ||
| if wa == 2 { | ||
| return std::cmp::Ordering::Greater; | ||
| } | ||
| if wb == 2 { | ||
| return std::cmp::Ordering::Less; | ||
| } | ||
| if wa != wb { | ||
| if wa < wb { | ||
| score += 1; | ||
| } else { | ||
| score -= 1; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if score != 0 { | ||
| return if score > 0 { | ||
| std::cmp::Ordering::Less | ||
| } else { | ||
| std::cmp::Ordering::Greater | ||
| }; | ||
| } | ||
|
|
||
| lexical_order_a.cmp(&lexical_order_b) | ||
|
Comment on lines
+224
to
+265
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this whole logic is here? Why can't you just compare the weights vectors?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A direct comparison of the weight vectors is purely lexicographic it only considers the first segment where the routes differ and ignores everything after that. For example:
The first difference is 2 (static) vs {id} (parameter), so a lexicographic comparison immediately ranks the first route ahead of the second. However, the second route is actually more specific overall. Let me know, if Iam wrong @seqre . |
||
| } | ||
|
|
||
| impl Display for PathMatcher { | ||
|
|
@@ -302,6 +388,7 @@ impl<'matcher, 'path> CaptureResult<'matcher, 'path> { | |
| enum PathPart { | ||
| Literal(String), | ||
| Param { name: String }, | ||
| Wildcard { name: String }, | ||
| } | ||
|
|
||
| impl Display for PathPart { | ||
|
|
@@ -312,6 +399,7 @@ impl Display for PathPart { | |
| write!(f, "{s}") | ||
| } | ||
| PathPart::Param { name } => write!(f, "{{{name}}}"), | ||
| PathPart::Wildcard { name } => write!(f, "{{*{name}}}"), | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -543,4 +631,55 @@ 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_not_allowed() { | ||
| let path_parser = PathMatcher::new("/users/rand/{*path}"); | ||
| assert_eq!(path_parser.capture("/users/rand/"), None); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "Wildcard must be the last part of the path: `/users/{*rest}/`")] | ||
| fn path_parser_no_path_allowed_after_wildcard() { | ||
| let _ = PathMatcher::new("/users/{*rest}/"); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.