Skip to content

Commit 5401a50

Browse files
committed
error on prefix-suffix conflicts
1 parent 83da717 commit 5401a50

File tree

7 files changed

+522
-116
lines changed

7 files changed

+522
-116
lines changed

README.md

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,62 +29,91 @@ The router supports dynamic route segments. These can either be named or catch-a
2929
Named parameters like `/{id}` match anything until the next static segment or the end of the path.
3030

3131
```rust,ignore
32-
let mut m = Router::new();
33-
m.insert("/users/{id}", true)?;
32+
let mut router = Router::new();
33+
router.insert("/users/{id}", 42)?;
3434
35-
assert_eq!(m.at("/users/1")?.params.get("id"), Some("1"));
36-
assert_eq!(m.at("/users/23")?.params.get("id"), Some("23"));
37-
assert!(m.at("/users").is_err());
35+
let matched = router.at("/users/1")?;
36+
assert_eq!(matched.params.get("id"), Some("1"));
37+
38+
let matched = router.at("/users/23")?;
39+
assert_eq!(matched.params.get("id"), Some("23"));
40+
41+
assert!(router.at("/users").is_err());
3842
```
3943

4044
Prefixes and suffixes within a segment are also supported. However, there may only be a single named parameter per route segment.
4145
```rust,ignore
42-
let mut m = Router::new();
43-
m.insert("/images/img{id}.png", true)?;
46+
let mut router = Router::new();
47+
router.insert("/images/img-{id}.png", true)?;
48+
49+
let matched = router.at("/images/img-1.png")?;
50+
assert_eq!(matched.params.get("id"), Some("1"));
4451
45-
assert_eq!(m.at("/images/img1.png")?.params.get("id"), Some("1"));
46-
assert!(m.at("/images/img1.jpg").is_err());
52+
assert!(router.at("/images/img-1.jpg").is_err());
4753
```
4854

49-
Catch-all parameters start with `*` and match anything until the end of the path. They must always be at the **end** of the route.
55+
Catch-all parameters start with a `*` and match anything until the end of the path. They must always be at the *end* of the route.
5056

5157
```rust,ignore
52-
let mut m = Router::new();
53-
m.insert("/{*p}", true)?;
58+
let mut router = Router::new();
59+
router.insert("/{*rest}", true)?;
5460
55-
assert_eq!(m.at("/foo.js")?.params.get("p"), Some("foo.js"));
56-
assert_eq!(m.at("/c/bar.css")?.params.get("p"), Some("c/bar.css"));
61+
let matched = router.at("/foo.html")?;
62+
assert_eq!(matched.params.get("rest"), Some("foo.html"));
5763
58-
// Note that this would lead to an empty parameter.
59-
assert!(m.at("/").is_err());
64+
let matched = router.at("/static/bar.css")?;
65+
assert_eq!(matched.params.get("rest"), Some("static/bar.css"));
66+
67+
// Note that this would lead to an empty parameter value.
68+
assert!(router.at("/").is_err());
6069
```
6170

6271
The literal characters `{` and `}` may be included in a static route by escaping them with the same character.
63-
For example, the `{` character is escaped with `{{` and the `}` character is escaped with `}}`.
72+
For example, the `{` character is escaped with `{{`, and the `}` character is escaped with `}}`.
6473

6574
```rust,ignore
66-
let mut m = Router::new();
67-
m.insert("/{{hello}}", true)?;
68-
m.insert("/{hello}", true)?;
75+
let mut router = Router::new();
76+
router.insert("/{{hello}}", true)?;
77+
router.insert("/{hello}", true)?;
6978
7079
// Match the static route.
71-
assert!(m.at("/{hello}")?.value);
80+
let matched = router.at("/{hello}")?;
81+
assert!(matched.params.is_empty());
7282
7383
// Match the dynamic route.
74-
assert_eq!(m.at("/hello")?.params.get("hello"), Some("hello"));
84+
let matched = router.at("/hello")?;
85+
assert_eq!(matched.params.get("hello"), Some("hello"));
7586
```
7687

77-
## Routing Priority
88+
## Conflict Rules
7889

7990
Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority:
8091

8192
```rust,ignore
82-
let mut m = Router::new();
83-
m.insert("/", "Welcome!").unwrap(); // Priority: 1
84-
m.insert("/about", "About Me").unwrap(); // Priority: 1
85-
m.insert("/{*filepath}", "...").unwrap(); // Priority: 2
93+
let mut router = Router::new();
94+
router.insert("/", "Welcome!").unwrap(); // Priority: 1
95+
router.insert("/about", "About Me").unwrap(); // Priority: 1
96+
router.insert("/{*filepath}", "...").unwrap(); // Priority: 2
8697
```
8798

99+
Formally, a route consists of a list of segments separated by `/`, with an optional leading and trailing slash: `(/)<segment_1>/.../<segment_n>(/)`.
100+
101+
Given set of routes, their overlapping segments may include, in order of priority:
102+
103+
- Any number of static segments (`/a`, `/b`, ...).
104+
- *One* of the following:
105+
- Any number of route parameters with a suffix (`/{x}a`, `/{x}b`, ...), prioritizing the longest suffix.
106+
- Any number of route parameters with a prefix (`/a{x}`, `/b{x}`, ...), prioritizing the longest prefix.
107+
- A single route parameter with both a prefix and a suffix (`/a{x}b`).
108+
- *One* of the following;
109+
- A single standalone parameter (`/{x}`).
110+
- A single standalone catch-all parameter (`/{*rest}`).
111+
112+
Any other combination of route segments is considered ambiguous, and attempting to insert such a route will result in an error.
113+
114+
The one exception to the above set of rules is that catch-all parameters are always considered to conflict with suffixed route parameters, i.e. that `/{*rest}`
115+
and `/{x}suffix` are overlapping. This is due to an implementation detail of the routing tree that may be relaxed in the future.
116+
88117
## How does it work?
89118

90119
The router takes advantage of the fact that URL routes generally follow a hierarchical structure.

src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ impl InsertError {
7979
route.append(&current.prefix);
8080
}
8181

82-
// Add the prefixes of any conflicting children.
82+
// Add the prefixes of the first conflicting child.
8383
let mut child = current.children.first();
8484
while let Some(node) = child {
8585
route.append(&node.prefix);

src/lib.rs

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ Named parameters like `/{id}` match anything until the next static segment or th
2626
```rust
2727
# use matchit::Router;
2828
# fn main() -> Result<(), Box<dyn std::error::Error>> {
29-
let mut m = Router::new();
30-
m.insert("/users/{id}", true)?;
29+
let mut router = Router::new();
30+
router.insert("/users/{id}", 42)?;
3131
32-
assert_eq!(m.at("/users/1")?.params.get("id"), Some("1"));
33-
assert_eq!(m.at("/users/23")?.params.get("id"), Some("23"));
34-
assert!(m.at("/users").is_err());
32+
let matched = router.at("/users/1")?;
33+
assert_eq!(matched.params.get("id"), Some("1"));
34+
35+
let matched = router.at("/users/23")?;
36+
assert_eq!(matched.params.get("id"), Some("23"));
37+
38+
assert!(router.at("/users").is_err());
3539
# Ok(())
3640
# }
3741
```
@@ -40,65 +44,90 @@ Prefixes and suffixes within a segment are also supported. However, there may on
4044
```rust
4145
# use matchit::Router;
4246
# fn main() -> Result<(), Box<dyn std::error::Error>> {
43-
let mut m = Router::new();
44-
m.insert("/images/img{id}.png", true)?;
47+
let mut router = Router::new();
48+
router.insert("/images/img-{id}.png", true)?;
49+
50+
let matched = router.at("/images/img-1.png")?;
51+
assert_eq!(matched.params.get("id"), Some("1"));
4552
46-
assert_eq!(m.at("/images/img1.png")?.params.get("id"), Some("1"));
47-
assert!(m.at("/images/img1.jpg").is_err());
53+
assert!(router.at("/images/img-1.jpg").is_err());
4854
# Ok(())
4955
# }
5056
```
5157
52-
Catch-all parameters start with `*` and match anything until the end of the path. They must always be at the **end** of the route.
58+
Catch-all parameters start with a `*` and match anything until the end of the path. They must always be at the *end* of the route.
5359
5460
```rust
5561
# use matchit::Router;
5662
# fn main() -> Result<(), Box<dyn std::error::Error>> {
57-
let mut m = Router::new();
58-
m.insert("/{*p}", true)?;
63+
let mut router = Router::new();
64+
router.insert("/{*rest}", true)?;
5965
60-
assert_eq!(m.at("/foo.js")?.params.get("p"), Some("foo.js"));
61-
assert_eq!(m.at("/c/bar.css")?.params.get("p"), Some("c/bar.css"));
66+
let matched = router.at("/foo.html")?;
67+
assert_eq!(matched.params.get("rest"), Some("foo.html"));
6268
63-
// Note that this would lead to an empty parameter.
64-
assert!(m.at("/").is_err());
69+
let matched = router.at("/static/bar.css")?;
70+
assert_eq!(matched.params.get("rest"), Some("static/bar.css"));
71+
72+
// Note that this would lead to an empty parameter value.
73+
assert!(router.at("/").is_err());
6574
# Ok(())
6675
# }
6776
```
6877
69-
The literal characters `{` and `}` may be included in a static route by escaping them with the same character. For example, the `{` character is escaped with `{{` and the `}` character is escaped with `}}`.
78+
The literal characters `{` and `}` may be included in a static route by escaping them with the same character. For example, the `{` character is escaped with `{{`, and the `}` character is escaped with `}}`.
7079
7180
```rust
7281
# use matchit::Router;
7382
# fn main() -> Result<(), Box<dyn std::error::Error>> {
74-
let mut m = Router::new();
75-
m.insert("/{{hello}}", true)?;
76-
m.insert("/{hello}", true)?;
83+
let mut router = Router::new();
84+
router.insert("/{{hello}}", true)?;
85+
router.insert("/{hello}", true)?;
7786
7887
// Match the static route.
79-
assert!(m.at("/{hello}")?.value);
88+
let matched = router.at("/{hello}")?;
89+
assert!(matched.params.is_empty());
8090
8191
// Match the dynamic route.
82-
assert_eq!(m.at("/hello")?.params.get("hello"), Some("hello"));
92+
let matched = router.at("/hello")?;
93+
assert_eq!(matched.params.get("hello"), Some("hello"));
8394
# Ok(())
8495
# }
8596
```
8697
87-
# Routing Priority
98+
# Conflict Rules
8899
89100
Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority:
90101
91102
```rust
92103
# use matchit::Router;
93104
# fn main() -> Result<(), Box<dyn std::error::Error>> {
94-
let mut m = Router::new();
95-
m.insert("/", "Welcome!").unwrap(); // Priority: 1
96-
m.insert("/about", "About Me").unwrap(); // Priority: 1
97-
m.insert("/{*filepath}", "...").unwrap(); // Priority: 2
105+
let mut router = Router::new();
106+
router.insert("/", "Welcome!").unwrap(); // Priority: 1
107+
router.insert("/about", "About Me").unwrap(); // Priority: 1
108+
router.insert("/{*filepath}", "...").unwrap(); // Priority: 2
98109
# Ok(())
99110
# }
100111
```
101112
113+
Formally, a route consists of a list of segments separated by `/`, with an optional leading and trailing slash: `(/)<segment_1>/.../<segment_n>(/)`.
114+
115+
Given set of routes, their overlapping segments may include, in order of priority:
116+
117+
- Any number of static segments (`/a`, `/b`, ...).
118+
- *One* of the following:
119+
- Any number of route parameters with a suffix (`/{x}a`, `/{x}b`, ...), prioritizing the longest suffix.
120+
- Any number of route parameters with a prefix (`/a{x}`, `/b{x}`, ...), prioritizing the longest prefix.
121+
- A single route parameter with both a prefix and a suffix (`/a{x}b`).
122+
- *One* of the following;
123+
- A single standalone parameter (`/{x}`).
124+
- A single standalone catch-all parameter (`/{*rest}`).
125+
126+
Any other combination of route segments is considered ambiguous, and attempting to insert such a route will result in an error.
127+
128+
The one exception to the above set of rules is that catch-all parameters are always considered to conflict with suffixed route parameters, i.e. that `/{*rest}`
129+
and `/{x}suffix` are overlapping. This is due to an implementation detail of the routing tree that may be relaxed in the future.
130+
102131
# How does it work?
103132
104133
The router takes advantage of the fact that URL routes generally follow a hierarchical structure. Routes are stored them in a radix trie that makes heavy use of common prefixes.

src/router.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ impl<T> Router<T> {
125125
///
126126
/// router.insert("/home/{id}/", "Hello!");
127127
/// // Invalid route.
128-
/// assert_eq!(router.remove("/home/{id"), None);
128+
/// assert_eq!(router.remove("/home/{id}"), None);
129129
/// assert_eq!(router.remove("/home/{id}/"), Some("Hello!"));
130130
/// ```
131131
pub fn remove(&mut self, path: impl Into<String>) -> Option<T> {

0 commit comments

Comments
 (0)