Skip to content

Commit 2aa0ba7

Browse files
committed
add support for parameter suffixes
1 parent 8913972 commit 2aa0ba7

File tree

4 files changed

+163
-19
lines changed

4 files changed

+163
-19
lines changed

src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ pub enum InsertError {
1414
},
1515
/// Only one parameter per route segment is allowed.
1616
///
17-
/// Static segments are also allowed before a parameter, but not after it. For example,
18-
/// `/foo-{bar}` is a valid route, but `/{bar}-foo` is not.
17+
/// For example, `/foo-{bar}` and `/{bar}-foo` are valid routes, but `/{foo}-{bar}`
18+
/// is not.
1919
InvalidParamSegment,
2020
/// Parameters must be registered with a valid name and matching braces.
2121
///

src/tree.rs

Lines changed: 117 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ pub(crate) enum NodeType {
3838
Root,
3939
/// A route parameter, e.g. `/{id}`.
4040
Param,
41+
/// A route parameter that is followed by a static suffix
42+
/// before a trailing slash, ex: `/{id}.png`.
43+
ParamSuffix { suffix_start: usize },
4144
/// A catch-all parameter, e.g. `/*file`.
4245
CatchAll,
4346
/// A static prefix, e.g. `/foo`.
@@ -127,7 +130,11 @@ impl<T> Node<T> {
127130
// After matching against a wildcard the next character is always `/`.
128131
//
129132
// Continue searching in the child node if it already exists.
130-
if current.node_type == NodeType::Param && current.children.len() == 1 {
133+
if matches!(
134+
current.node_type,
135+
NodeType::Param | NodeType::ParamSuffix { .. }
136+
) && current.children.len() == 1
137+
{
131138
debug_assert_eq!(next, b'/');
132139
current = &mut current.children[0];
133140
current.priority += 1;
@@ -173,11 +180,14 @@ impl<T> Node<T> {
173180
current = current.children.last_mut().unwrap();
174181
current.priority += 1;
175182

183+
let segment = remaining
184+
.iter()
185+
.position(|b| *b == b'/')
186+
.unwrap_or(remaining.len());
187+
176188
// Make sure the route parameter matches.
177-
if let Some(wildcard) = remaining.get(..current.prefix.len()) {
178-
if *wildcard != *current.prefix {
179-
return Err(InsertError::conflict(&route, remaining, current));
180-
}
189+
if remaining[..segment] != *current.prefix {
190+
return Err(InsertError::conflict(&route, remaining, current));
181191
}
182192

183193
// Catch-all routes cannot have children.
@@ -402,10 +412,39 @@ impl<T> Node<T> {
402412
prefix = prefix.slice_off(wildcard.start);
403413
}
404414

415+
let (node_type, wildcard) = match prefix.get(wildcard.len()) {
416+
// The entire route segment consists of the wildcard.
417+
None | Some(&b'/') => {
418+
let wildcard_prefix = prefix.slice_until(wildcard.len());
419+
prefix = prefix.slice_off(wildcard_prefix.len());
420+
(NodeType::Param, wildcard_prefix)
421+
}
422+
// The route parameter is followed a static suffix within the current segment.
423+
_ => {
424+
let end = prefix
425+
.iter()
426+
.position(|&b| b == b'/')
427+
.unwrap_or(prefix.len());
428+
429+
let wildcard_prefix = prefix.slice_until(end);
430+
let suffix = wildcard_prefix.slice_off(wildcard.len());
431+
432+
// Multiple parameters within the same segment, e.g. `/{foo}{bar}`.
433+
if matches!(find_wildcard(suffix), Ok(Some(_))) {
434+
return Err(InsertError::InvalidParamSegment);
435+
}
436+
437+
prefix = prefix.slice_off(end);
438+
439+
let suffix_start = wildcard.len();
440+
(NodeType::ParamSuffix { suffix_start }, wildcard_prefix)
441+
}
442+
};
443+
405444
// Add the parameter as a child node.
406445
let child = Self {
407-
node_type: NodeType::Param,
408-
prefix: prefix.slice_until(wildcard.len()).to_owned(),
446+
node_type,
447+
prefix: wildcard.to_owned(),
409448
..Self::default()
410449
};
411450

@@ -415,8 +454,7 @@ impl<T> Node<T> {
415454
current.priority += 1;
416455

417456
// If the route doesn't end in the wildcard, we have to insert the suffix as a child.
418-
if wildcard.len() < prefix.len() {
419-
prefix = prefix.slice_off(wildcard.len());
457+
if !prefix.is_empty() {
420458
let child = Self {
421459
priority: 1,
422460
..Self::default()
@@ -616,6 +654,76 @@ impl<T> Node<T> {
616654
// Otherwise, there are no matching routes in the tree.
617655
return Err(MatchError::NotFound);
618656
}
657+
NodeType::ParamSuffix { suffix_start } => {
658+
let suffix = &current.prefix[suffix_start..];
659+
660+
// Double `//` implying an empty parameter, no match.
661+
if path[0] == b'/' {
662+
try_backtrack!();
663+
return Err(MatchError::NotFound);
664+
}
665+
666+
// The path cannot contain the suffix.
667+
if path.len() <= suffix.len() {
668+
try_backtrack!();
669+
return Err(MatchError::NotFound);
670+
}
671+
672+
// Skip the first path byte to ensure a non-empty parameter.
673+
for i in 1..=(path.len() - suffix.len()) {
674+
// Reached a `/` before the suffix, no match.
675+
if path[i] == b'/' {
676+
try_backtrack!();
677+
return Err(MatchError::NotFound);
678+
}
679+
680+
// Ensure the suffix matches.
681+
if path[i..][..suffix.len()] != *suffix {
682+
continue;
683+
}
684+
685+
let param = &path[..i];
686+
let rest = &path[i + suffix.len()..];
687+
688+
if rest.is_empty() {
689+
let value = match current.value {
690+
// Found the matching value.
691+
Some(ref value) => value,
692+
// Otherwise, this route does not match.
693+
None => {
694+
try_backtrack!();
695+
return Err(MatchError::NotFound);
696+
}
697+
};
698+
699+
// Store the parameter value.
700+
// Parameters are normalized so the key is irrelevant for now.
701+
params.push(b"", param);
702+
703+
// Remap the keys of any route parameters we accumulated during the search.
704+
params.for_each_key_mut(|(i, key)| *key = &current.remapping[i]);
705+
706+
return Ok((value, params));
707+
}
708+
709+
// If there is a static child, continue the search.
710+
if let [child] = current.children.as_slice() {
711+
// Store the parameter value.
712+
// Parameters are normalized so the key is irrelevant for now.
713+
params.push(b"", param);
714+
715+
// Continue searching.
716+
path = rest;
717+
current = child;
718+
backtracking = false;
719+
continue 'walk;
720+
}
721+
722+
// Otherwise, this route does not match.
723+
try_backtrack!();
724+
return Err(MatchError::NotFound);
725+
}
726+
}
619727
NodeType::CatchAll => {
620728
// Catch-all segments are only allowed at the end of the route, meaning
621729
// this node must contain the value.
@@ -785,13 +893,6 @@ fn find_wildcard(path: UnescapedRef<'_>) -> Result<Option<Range<usize>>, InsertE
785893
return Err(InsertError::InvalidParam);
786894
}
787895

788-
if let Some(&c) = path.get(i + 1) {
789-
// Prefixes after route parameters are not supported.
790-
if c != b'/' {
791-
return Err(InsertError::InvalidParamSegment);
792-
}
793-
}
794-
795896
return Ok(Some(start..i + 1));
796897
}
797898
// `*` and `/` are invalid in parameter names.

tests/insert.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ fn wildcard_conflict() {
4848
("/user_{bar}", Err(conflict("/user_{name}"))),
4949
("/id{id}", Ok(())),
5050
("/id/{id}", Ok(())),
51+
("/x/{id}", Ok(())),
52+
("/x/{id}/", Ok(())),
53+
("/x/{id}y", Err(conflict("/x/{id}/"))),
54+
("/x/{id}y/", Err(conflict("/x/{id}/"))),
55+
("/x/x{id}", Ok(())),
56+
("/x/x{id}y", Err(conflict("/x/x{id}"))),
57+
("/qux/id", Ok(())),
58+
("/qux/{id}y", Ok(())),
59+
("/qux/{id}", Err(conflict("/qux/{id}y"))),
60+
("/qux/{id}/", Err(conflict("/qux/{id}y"))),
61+
("/qux/{id}x", Err(conflict("/qux/{id}y"))),
62+
("/qux/x{id}y", Ok(())),
63+
("/qux/x{id}", Err(conflict("/qux/x{id}y"))),
5164
])
5265
.run()
5366
}
@@ -210,7 +223,6 @@ fn invalid_param() {
210223
("}", Err(InsertError::InvalidParam)),
211224
("x{y", Err(InsertError::InvalidParam)),
212225
("x}", Err(InsertError::InvalidParam)),
213-
("/{foo}s", Err(InsertError::InvalidParamSegment)),
214226
])
215227
.run();
216228
}

tests/match.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,37 @@ fn empty_param() {
591591
.run();
592592
}
593593

594+
#[test]
595+
fn wildcard_suffix() {
596+
MatchTest {
597+
routes: vec![
598+
"/",
599+
"/{foo}x",
600+
"/foox",
601+
"/{foo}x/bar",
602+
"/{foo}x/bar/baz",
603+
"/x{foo}",
604+
"/x{foo}/bar",
605+
],
606+
matches: vec![
607+
("/", "/", p! {}),
608+
("/foox", "/foox", p! {}),
609+
("/barx", "/{foo}x", p! { "foo" => "bar" }),
610+
("/mx", "/{foo}x", p! { "foo" => "m" }),
611+
("/mx/", "", Err(())),
612+
("/mxm", "", Err(())),
613+
("/mx/bar", "/{foo}x/bar", p! { "foo" => "m" }),
614+
("/mxm/bar", "", Err(())),
615+
("/x", "", Err(())),
616+
("/xfoo", "/x{foo}", p! { "foo" => "foo" }),
617+
("/xfoox", "/x{foo}", p! { "foo" => "foox" }),
618+
("/xfoox/bar", "/x{foo}/bar", p! { "foo" => "foox" }),
619+
("/xfoox/bar/baz", "/{foo}x/bar/baz", p! { "foo" => "xfoo" }),
620+
],
621+
}
622+
.run();
623+
}
624+
594625
#[test]
595626
fn basic() {
596627
MatchTest {

0 commit comments

Comments
 (0)