Skip to content

Commit 3ce31bc

Browse files
committed
add fast-path for bare route parameters
1 parent 4519a6b commit 3ce31bc

File tree

3 files changed

+89
-19
lines changed

3 files changed

+89
-19
lines changed

src/escape.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{fmt, ops::Range};
22

3-
/// An unescaped route that keeps track of the position of escaped characters ('{{' or '}}').
3+
/// An unescaped route that keeps track of the position of
4+
/// escaped characters, i.e. '{{' or '}}'.
45
///
56
/// Note that this type dereferences to `&[u8]`.
67
#[derive(Clone, Default)]
@@ -107,7 +108,7 @@ impl fmt::Debug for UnescapedRoute {
107108
/// A reference to an `UnescapedRoute`.
108109
#[derive(Copy, Clone)]
109110
pub struct UnescapedRef<'a> {
110-
pub inner: &'a [u8],
111+
inner: &'a [u8],
111112
escaped: &'a [usize],
112113
// An offset applied to each escaped index.
113114
offset: isize,

src/tree.rs

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,16 @@ pub(crate) enum NodeType {
4848

4949
/// A route parameter, e.g. '/{id}'.
5050
///
51-
/// The leaves of a parameter node are suffixes within
52-
/// the segment, i.e. before the next '/', sorted by length.
53-
/// This allows for a reverse linear search to determine the
54-
/// correct leaf. It would also be possible to use a reverse
55-
/// prefix-tree here, but is likely not worth the complexity.
56-
Param,
51+
/// If `suffix` is `false`, the only child of this node is
52+
/// a static '/', allowing for a fast path when searching.
53+
/// Otherwise, the route may have static suffixes, e.g. '/{id}.png'.
54+
///
55+
/// The leaves of a parameter node are the static suffixes
56+
/// sorted by length. This allows for a reverse linear search
57+
/// to determine the correct leaf. It would also be possible to
58+
/// use a reverse prefix-tree here, but is likely not worth the
59+
/// complexity.
60+
Param { suffix: bool },
5761

5862
/// A catch-all parameter, e.g. '/{*file}'.
5963
CatchAll,
@@ -144,7 +148,7 @@ impl<T> Node<T> {
144148

145149
// For parameters with a suffix, we have to find the matching suffix or
146150
// create a new child node.
147-
if current.node_type == NodeType::Param {
151+
if matches!(current.node_type, NodeType::Param { .. }) {
148152
let terminator = remaining
149153
.iter()
150154
.position(|&b| b == b'/')
@@ -169,14 +173,13 @@ impl<T> Node<T> {
169173
}
170174

171175
// If there is no matching suffix, create a new suffix node.
172-
let child = Node {
176+
let child = current.add_suffix_child(Node {
173177
prefix: suffix.to_owned(),
174178
node_type: NodeType::Static,
175179
priority: 1,
176180
..Node::default()
177-
};
178-
179-
let child = current.add_suffix_child(child);
181+
});
182+
current.node_type = NodeType::Param { suffix: true };
180183
current = &mut current.children[child];
181184

182185
// If this is the final route segment, insert the value.
@@ -477,8 +480,7 @@ impl<T> Node<T> {
477480
}
478481

479482
// Otherwise, we're inserting a regular route parameter.
480-
assert_eq!(prefix[wildcard.clone()][0], b'{');
481-
483+
//
482484
// Add the prefix before the wildcard into the current node.
483485
if wildcard.start > 0 {
484486
current.prefix = prefix.slice_until(wildcard.start).to_owned();
@@ -503,17 +505,25 @@ impl<T> Node<T> {
503505
}
504506

505507
// Add the parameter as a child node.
508+
let has_suffix = !matches!(*suffix, b"" | b"/");
506509
let child = current.add_child(Node {
507510
priority: 1,
508-
node_type: NodeType::Param,
511+
node_type: NodeType::Param { suffix: has_suffix },
509512
prefix: wildcard.to_owned(),
510513
..Node::default()
511514
});
512515

513516
current.wild_child = true;
514517
current = &mut current.children[child];
515518

516-
// Add the static suffix before the '/', if there is one.
519+
// Add the static suffix until the '/', if there is one.
520+
//
521+
// Note that for '/' suffixes where `suffix: false`, this
522+
// unconditionally introduces an extra node for the '/'
523+
// without attempting to merge with the remaining route.
524+
// This makes converting a non-suffix parameter node into
525+
// a suffix one easier during insertion, but slightly hurts
526+
// performance.
517527
if !suffix.is_empty() {
518528
let child = current.add_suffix_child(Node {
519529
priority: 1,
@@ -534,7 +544,6 @@ impl<T> Node<T> {
534544
// If there is a static segment after the '/', setup the node
535545
// for the rest of the route.
536546
if prefix[0] != b'{' || prefix.is_escaped(0) {
537-
assert!(prefix[0] != b'{');
538547
current.indices.push(prefix[0]);
539548
let child = current.add_child(Node {
540549
priority: 1,
@@ -638,7 +647,55 @@ impl<T> Node<T> {
638647
// Continue searching in the wildcard child, which is kept at the end of the list.
639648
node = node.children.last().unwrap();
640649
match node.node_type {
641-
NodeType::Param => {
650+
NodeType::Param { suffix: false } => {
651+
// Check for more path segments.
652+
let terminator = match path.iter().position(|&c| c == b'/') {
653+
// Double `//` implying an empty parameter, no match.
654+
Some(0) => break 'walk,
655+
656+
// Found another segment.
657+
Some(i) => i,
658+
659+
// This is the last path segment.
660+
None => {
661+
// If this is the last path segment and there is a matching
662+
// value without a suffix, we have a match.
663+
let Some(ref value) = node.value else {
664+
break 'walk;
665+
};
666+
667+
// Store the parameter value.
668+
// Parameters are normalized so the key is irrelevant for now.
669+
params.push(b"", path);
670+
671+
// Remap the keys of any route parameters we accumulated during the
672+
params
673+
.for_each_key_mut(|(i, param)| param.key = &node.remapping[i]);
674+
675+
return Ok((value, params));
676+
}
677+
};
678+
679+
// Found another path segment.
680+
let (param, rest) = path.split_at(terminator);
681+
682+
// If there is a static child, continue the search.
683+
let [child] = node.children.as_slice() else {
684+
break 'walk;
685+
};
686+
687+
// Store the parameter value.
688+
// Parameters are normalized so the key is irrelevant for now.
689+
params.push(b"", param);
690+
691+
// Continue searching.
692+
path = rest;
693+
node = child;
694+
backtracking = false;
695+
continue 'walk;
696+
}
697+
698+
NodeType::Param { suffix: true } => {
642699
// Check for more path segments.
643700
let slash = path.iter().position(|&c| c == b'/');
644701
let terminator = match slash {

tests/match.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,12 @@ fn mixed_wildcard_suffix() {
648648
"/foo/{b}one/one",
649649
"/foo/{b}two/one",
650650
"/foo/{b}one/one/",
651+
"/bar/{b}one",
652+
"/bar/{b}",
653+
"/bar/{b}/baz",
654+
"/bar/{b}one/baz",
655+
"/baz/{b}/bar",
656+
"/baz/{b}one/bar",
651657
],
652658
matches: vec![
653659
("/", "/", p! {}),
@@ -661,6 +667,12 @@ fn mixed_wildcard_suffix() {
661667
("/foo/bone/one", "/foo/{b}one/one", p! { "b" => "b" }),
662668
("/foo/bone/one/", "/foo/{b}one/one/", p! { "b" => "b" }),
663669
("/foo/btwo/one", "/foo/{b}two/one", p! { "b" => "b" }),
670+
("/bar/b", "/bar/{b}", p! { "b" => "b" }),
671+
("/bar/b/baz", "/bar/{b}/baz", p! { "b" => "b" }),
672+
("/bar/bone", "/bar/{b}one", p! { "b" => "b" }),
673+
("/bar/bone/baz", "/bar/{b}one/baz", p! { "b" => "b" }),
674+
("/baz/b/bar", "/baz/{b}/bar", p! { "b" => "b" }),
675+
("/baz/bone/bar", "/baz/{b}one/bar", p! { "b" => "b" }),
664676
],
665677
}
666678
.run();

0 commit comments

Comments
 (0)