Skip to content

Commit efb488e

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

File tree

4 files changed

+171
-20
lines changed

4 files changed

+171
-20
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: 116 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,75 @@ 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+
// Check for more path segments.
661+
let end = match path.iter().position(|&c| c == b'/') {
662+
// Double `//` implying an empty parameter, no match.
663+
Some(0) => {
664+
try_backtrack!();
665+
return Err(MatchError::NotFound);
666+
}
667+
// Found another segment.
668+
Some(i) => i,
669+
// This is the last path segment.
670+
None => path.len(),
671+
};
672+
673+
// The path cannot contain a non-empty parameter and the suffix.
674+
if suffix.len() >= end {
675+
try_backtrack!();
676+
return Err(MatchError::NotFound);
677+
}
678+
679+
// Ensure the suffix matches.
680+
for (a, b) in path[..end].iter().rev().zip(suffix.iter().rev()) {
681+
if a != b {
682+
try_backtrack!();
683+
return Err(MatchError::NotFound);
684+
}
685+
}
686+
687+
let param = &path[..end - suffix.len()];
688+
let rest = &path[end..];
689+
690+
if rest.is_empty() {
691+
let value = match current.value {
692+
// Found the matching value.
693+
Some(ref value) => value,
694+
// Otherwise, this route does not match.
695+
None => {
696+
try_backtrack!();
697+
return Err(MatchError::NotFound);
698+
}
699+
};
700+
701+
// Store the parameter value.
702+
params.push(b"", param);
703+
704+
// Remap the keys of any route parameters we accumulated during the search.
705+
params.for_each_key_mut(|(i, key)| *key = &current.remapping[i]);
706+
707+
return Ok((value, params));
708+
}
709+
710+
// If there is a static child, continue the search.
711+
if let [child] = current.children.as_slice() {
712+
// Store the parameter value.
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+
}
619726
NodeType::CatchAll => {
620727
// Catch-all segments are only allowed at the end of the route, meaning
621728
// this node must contain the value.
@@ -785,13 +892,6 @@ fn find_wildcard(path: UnescapedRef<'_>) -> Result<Option<Range<usize>>, InsertE
785892
return Err(InsertError::InvalidParam);
786893
}
787894

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-
795895
return Ok(Some(start..i + 1));
796896
}
797897
// `*` 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: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,11 +581,50 @@ fn escaped() {
581581
#[test]
582582
fn empty_param() {
583583
MatchTest {
584-
routes: vec!["/y/{foo}", "/x/{foo}/z", "/z/{*xx}"],
584+
routes: vec![
585+
"/y/{foo}",
586+
"/x/{foo}/z",
587+
"/z/{*foo}",
588+
"/a/x{foo}",
589+
"/b/{foo}x",
590+
],
585591
matches: vec![
586592
("/y/", "", Err(())),
587593
("/x//z", "", Err(())),
588594
("/z/", "", Err(())),
595+
("/a/x", "", Err(())),
596+
("/b/x", "", Err(())),
597+
],
598+
}
599+
.run();
600+
}
601+
602+
#[test]
603+
fn wildcard_suffix() {
604+
MatchTest {
605+
routes: vec![
606+
"/",
607+
"/{foo}x",
608+
"/foox",
609+
"/{foo}x/bar",
610+
"/{foo}x/bar/baz",
611+
"/x{foo}",
612+
"/x{foo}/bar",
613+
],
614+
matches: vec![
615+
("/", "/", p! {}),
616+
("/foox", "/foox", p! {}),
617+
("/barx", "/{foo}x", p! { "foo" => "bar" }),
618+
("/mx", "/{foo}x", p! { "foo" => "m" }),
619+
("/mx/", "", Err(())),
620+
("/mxm", "", Err(())),
621+
("/mx/bar", "/{foo}x/bar", p! { "foo" => "m" }),
622+
("/mxm/bar", "", Err(())),
623+
("/x", "", Err(())),
624+
("/xfoo", "/x{foo}", p! { "foo" => "foo" }),
625+
("/xfoox", "/x{foo}", p! { "foo" => "foox" }),
626+
("/xfoox/bar", "/x{foo}/bar", p! { "foo" => "foox" }),
627+
("/xfoox/bar/baz", "/{foo}x/bar/baz", p! { "foo" => "xfoo" }),
589628
],
590629
}
591630
.run();

0 commit comments

Comments
 (0)