├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── LICENSE.httprouter ├── README.md ├── benches └── bench.rs ├── examples └── hyper.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── insert_and_match.rs ├── src ├── error.rs ├── escape.rs ├── lib.rs ├── params.rs ├── router.rs └── tree.rs └── tests ├── insert.rs ├── match.rs ├── merge.rs └── remove.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Rust 4 | 5 | jobs: 6 | check: 7 | name: Cargo Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: 1.66 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | args: --all-targets --all-features 34 | 35 | doc: 36 | name: Documentation 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions-rs/toolchain@v1 41 | with: 42 | profile: minimal 43 | toolchain: stable 44 | override: true 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: test 48 | args: --doc 49 | 50 | fmt: 51 | name: Format 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add rustfmt 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: fmt 64 | args: --all -- --check 65 | 66 | clippy: 67 | name: Clippy Lints 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v2 71 | - uses: actions-rs/toolchain@v1 72 | with: 73 | profile: minimal 74 | toolchain: stable 75 | override: true 76 | - run: rustup component add clippy 77 | - uses: actions-rs/cargo@v1 78 | with: 79 | command: clippy 80 | args: --all-targets --all-features -- --warn warnings --warn clippy::all --warn clippy::pedantic --warn clippy::cargo --warn clippy::nursery 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition="2018" 2 | reorder_imports=true 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "matchit" 3 | version = "0.8.6" 4 | license = "MIT AND BSD-3-Clause" 5 | authors = ["Ibraheem Ahmed "] 6 | edition = "2021" 7 | description = "A high performance, zero-copy URL router." 8 | categories = ["network-programming", "algorithms"] 9 | keywords = ["router", "path", "tree", "match", "url"] 10 | repository = "https://github.com/ibraheemdev/matchit" 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | 15 | [dev-dependencies] 16 | # Benchmarks 17 | criterion = "0.5" 18 | actix-router = "0.5" 19 | regex = "1" 20 | route-recognizer = "0.3" 21 | gonzales = "0.0.3-beta" 22 | path-tree = "0.8" 23 | routefinder = "0.5" 24 | wayfind = "0.8" 25 | 26 | # Examples 27 | tower = { version = "0.5.2", features = ["make", "util"] } 28 | tokio = { version = "1", features = ["full"] } 29 | http-body-util = "0.1" 30 | hyper = { version = "1", features = ["http1", "server"] } 31 | hyper-util = { version = "0.1", features = ["tokio"] } 32 | 33 | [features] 34 | default = [] 35 | __test_helpers = [] 36 | 37 | [[bench]] 38 | name = "bench" 39 | harness = false 40 | 41 | [profile.release] 42 | lto = true 43 | opt-level = 3 44 | codegen-units = 1 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ibraheem Ahmed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.httprouter: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2013, Julien Schmidt 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `matchit` 2 | 3 | [crates.io](https://crates.io/crates/matchit) 4 | [github](https://github.com/ibraheemdev/matchit) 5 | [docs.rs](https://docs.rs/matchit) 6 | 7 | A high performance, zero-copy URL router. 8 | 9 | ```rust 10 | use matchit::Router; 11 | 12 | fn main() -> Result<(), Box> { 13 | let mut router = Router::new(); 14 | router.insert("/home", "Welcome!")?; 15 | router.insert("/users/{id}", "A User")?; 16 | 17 | let matched = router.at("/users/978")?; 18 | assert_eq!(matched.params.get("id"), Some("978")); 19 | assert_eq!(*matched.value, "A User"); 20 | 21 | Ok(()) 22 | } 23 | ``` 24 | 25 | ## Parameters 26 | 27 | The router supports dynamic route segments. These can either be named or catch-all parameters. 28 | 29 | Named parameters like `/{id}` match anything until the next static segment or the end of the path. 30 | 31 | ```rust,ignore 32 | let mut m = Router::new(); 33 | m.insert("/users/{id}", true)?; 34 | 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()); 38 | ``` 39 | 40 | Prefixes and suffixes within a segment are also supported. However, there may only be a single named parameter per route segment. 41 | ```rust,ignore 42 | let mut m = Router::new(); 43 | m.insert("/images/img{id}.png", true)?; 44 | 45 | assert_eq!(m.at("/images/img1.png")?.params.get("id"), Some("1")); 46 | assert!(m.at("/images/img1.jpg").is_err()); 47 | ``` 48 | 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. 50 | 51 | ```rust,ignore 52 | let mut m = Router::new(); 53 | m.insert("/{*p}", true)?; 54 | 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")); 57 | 58 | // Note that this would lead to an empty parameter. 59 | assert!(m.at("/").is_err()); 60 | ``` 61 | 62 | 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 `}}`. 64 | 65 | ```rust,ignore 66 | let mut m = Router::new(); 67 | m.insert("/{{hello}}", true)?; 68 | m.insert("/{hello}", true)?; 69 | 70 | // Match the static route. 71 | assert!(m.at("/{hello}")?.value); 72 | 73 | // Match the dynamic route. 74 | assert_eq!(m.at("/hello")?.params.get("hello"), Some("hello")); 75 | ``` 76 | 77 | ## Routing Priority 78 | 79 | Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority: 80 | 81 | ```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 86 | ``` 87 | 88 | ## How does it work? 89 | 90 | The router takes advantage of the fact that URL routes generally follow a hierarchical structure. 91 | Routes are stored them in a radix trie that makes heavy use of common prefixes. 92 | 93 | ```text 94 | Priority Path Value 95 | 9 \ 1 96 | 3 ├s None 97 | 2 |├earch\ 2 98 | 1 |└upport\ 3 99 | 2 ├blog\ 4 100 | 1 | └{post} None 101 | 1 | └\ 5 102 | 2 ├about-us\ 6 103 | 1 | └team\ 7 104 | 1 └contact\ 8 105 | ``` 106 | 107 | This allows us to reduce the route search to a small number of branches. Child nodes on the same level of the tree are also 108 | prioritized by the number of children with registered values, increasing the chance of choosing the correct branch of the first try. 109 | 110 | ## Benchmarks 111 | 112 | As it turns out, this method of routing is extremely fast. Below are the benchmark results matching against 130 registered routes. 113 | You can view the benchmark code [here](https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs). 114 | 115 | ```text 116 | Compare Routers/matchit 117 | time: [2.4451 µs 2.4456 µs 2.4462 µs] 118 | 119 | Compare Routers/gonzales 120 | time: [4.2618 µs 4.2632 µs 4.2646 µs] 121 | 122 | Compare Routers/path-tree 123 | time: [4.8666 µs 4.8696 µs 4.8728 µs] 124 | 125 | Compare Routers/wayfind 126 | time: [4.9440 µs 4.9539 µs 4.9668 µs] 127 | 128 | Compare Routers/route-recognizer 129 | time: [49.203 µs 49.214 µs 49.226 µs] 130 | 131 | Compare Routers/routefinder 132 | time: [70.598 µs 70.636 µs 70.670 µs] 133 | 134 | Compare Routers/actix 135 | time: [453.91 µs 454.01 µs 454.11 µs] 136 | 137 | Compare Routers/regex 138 | time: [421.76 µs 421.82 µs 421.89 µs] 139 | ``` 140 | 141 | ## Credits 142 | 143 | A lot of the code in this package was inspired by Julien Schmidt's [`httprouter`](https://github.com/julienschmidt/httprouter). 144 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | 3 | fn compare_routers(c: &mut Criterion) { 4 | let mut group = c.benchmark_group("Compare Routers"); 5 | 6 | let paths = routes!(literal).to_vec(); 7 | 8 | let mut matchit = matchit::Router::new(); 9 | for route in routes!(brackets) { 10 | matchit.insert(route, true).unwrap(); 11 | } 12 | group.bench_function("matchit", |b| { 13 | b.iter(|| { 14 | for path in black_box(&paths) { 15 | let result = black_box(matchit.at(path).unwrap()); 16 | assert!(*result.value); 17 | } 18 | }); 19 | }); 20 | 21 | let mut wayfind = wayfind::Router::new(); 22 | for route in routes!(brackets) { 23 | wayfind.insert(route, true).unwrap(); 24 | } 25 | let wayfind_paths = paths.to_vec(); 26 | group.bench_function("wayfind", |b| { 27 | b.iter(|| { 28 | for path in black_box(&wayfind_paths) { 29 | let result = black_box(wayfind.search(path).unwrap()); 30 | assert!(*result.data); 31 | } 32 | }); 33 | }); 34 | 35 | let mut path_tree = path_tree::PathTree::new(); 36 | for route in routes!(colon) { 37 | let _ = path_tree.insert(route, true); 38 | } 39 | group.bench_function("path-tree", |b| { 40 | b.iter(|| { 41 | for path in black_box(&paths) { 42 | let result = black_box(path_tree.find(path).unwrap()); 43 | assert!(*result.0); 44 | } 45 | }); 46 | }); 47 | 48 | let registered = routes!(brackets); 49 | let gonzales = gonzales::RouterBuilder::new().build(registered); 50 | group.bench_function("gonzales", |b| { 51 | b.iter(|| { 52 | for path in black_box(&paths) { 53 | let result = black_box(gonzales.route(path).unwrap()); 54 | assert!(registered.get(result.get_index()).is_some()); 55 | } 56 | }); 57 | }); 58 | 59 | let mut actix = actix_router::Router::::build(); 60 | for route in routes!(brackets) { 61 | actix.path(route, true); 62 | } 63 | let actix = actix.finish(); 64 | group.bench_function("actix", |b| { 65 | b.iter(|| { 66 | for path in black_box(&paths) { 67 | let mut path = actix_router::Path::new(*path); 68 | let result = black_box(actix.recognize(&mut path).unwrap()); 69 | assert!(*result.0); 70 | } 71 | }); 72 | }); 73 | 74 | let regex_set = regex::RegexSet::new(routes!(regex)).unwrap(); 75 | group.bench_function("regex", |b| { 76 | b.iter(|| { 77 | for path in black_box(&paths) { 78 | let result = black_box(regex_set.matches(path)); 79 | assert!(result.matched_any()); 80 | } 81 | }); 82 | }); 83 | 84 | let mut route_recognizer = route_recognizer::Router::new(); 85 | for route in routes!(colon) { 86 | route_recognizer.add(route, true); 87 | } 88 | group.bench_function("route-recognizer", |b| { 89 | b.iter(|| { 90 | for path in black_box(&paths) { 91 | let result = black_box(route_recognizer.recognize(path).unwrap()); 92 | assert!(**result.handler()); 93 | } 94 | }); 95 | }); 96 | 97 | let mut routefinder = routefinder::Router::new(); 98 | for route in routes!(colon) { 99 | routefinder.add(route, true).unwrap(); 100 | } 101 | group.bench_function("routefinder", |b| { 102 | b.iter(|| { 103 | for path in black_box(&paths) { 104 | let result = black_box(routefinder.best_match(path).unwrap()); 105 | assert!(*result.handler()); 106 | } 107 | }); 108 | }); 109 | 110 | group.finish(); 111 | } 112 | 113 | criterion_group!(benches, compare_routers); 114 | criterion_main!(benches); 115 | 116 | macro_rules! routes { 117 | (literal) => {{ 118 | routes!(finish => "p1", "p2", "p3", "p4") 119 | }}; 120 | (colon) => {{ 121 | routes!(finish => ":p1", ":p2", ":p3", ":p4") 122 | }}; 123 | (brackets) => {{ 124 | routes!(finish => "{p1}", "{p2}", "{p3}", "{p4}") 125 | }}; 126 | (regex) => {{ 127 | routes!(finish => "(.*)", "(.*)", "(.*)", "(.*)") 128 | }}; 129 | (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ 130 | [ 131 | concat!("/authorizations"), 132 | concat!("/authorizations/", $p1), 133 | concat!("/applications/", $p1, "/tokens/", $p2), 134 | concat!("/events"), 135 | concat!("/repos/", $p1, "/", $p2, "/events"), 136 | concat!("/networks/", $p1, "/", $p2, "/events"), 137 | concat!("/orgs/", $p1, "/events"), 138 | concat!("/users/", $p1, "/received_events"), 139 | concat!("/users/", $p1, "/received_events/public"), 140 | concat!("/users/", $p1, "/events"), 141 | concat!("/users/", $p1, "/events/public"), 142 | concat!("/users/", $p1, "/events/orgs/", $p2), 143 | concat!("/feeds"), 144 | concat!("/notifications"), 145 | concat!("/repos/", $p1, "/", $p2, "/notifications"), 146 | concat!("/notifications/threads/", $p1), 147 | concat!("/notifications/threads/", $p1, "/subscription"), 148 | concat!("/repos/", $p1, "/", $p2, "/stargazers"), 149 | concat!("/users/", $p1, "/starred"), 150 | concat!("/user/starred"), 151 | concat!("/user/starred/", $p1, "/", $p2), 152 | concat!("/repos/", $p1, "/", $p2, "/subscribers"), 153 | concat!("/users/", $p1, "/subscriptions"), 154 | concat!("/user/subscriptions"), 155 | concat!("/repos/", $p1, "/", $p2, "/subscription"), 156 | concat!("/user/subscriptions/", $p1, "/", $p2), 157 | concat!("/users/", $p1, "/gists"), 158 | concat!("/gists"), 159 | concat!("/gists/", $p1), 160 | concat!("/gists/", $p1, "/star"), 161 | concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), 162 | concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), 163 | concat!("/repos/", $p1, "/", $p2, "/git/refs"), 164 | concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), 165 | concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), 166 | concat!("/issues"), 167 | concat!("/user/issues"), 168 | concat!("/orgs/", $p1, "/issues"), 169 | concat!("/repos/", $p1, "/", $p2, "/issues"), 170 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), 171 | concat!("/repos/", $p1, "/", $p2, "/assignees"), 172 | concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), 173 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), 174 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), 175 | concat!("/repos/", $p1, "/", $p2, "/labels"), 176 | concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), 177 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), 178 | concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), 179 | concat!("/repos/", $p1, "/", $p2, "/milestones/"), 180 | concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), 181 | concat!("/emojis"), 182 | concat!("/gitignore/templates"), 183 | concat!("/gitignore/templates/", $p1), 184 | concat!("/meta"), 185 | concat!("/rate_limit"), 186 | concat!("/users/", $p1, "/orgs"), 187 | concat!("/user/orgs"), 188 | concat!("/orgs/", $p1), 189 | concat!("/orgs/", $p1, "/members"), 190 | concat!("/orgs/", $p1, "/members/", $p2), 191 | concat!("/orgs/", $p1, "/public_members"), 192 | concat!("/orgs/", $p1, "/public_members/", $p2), 193 | concat!("/orgs/", $p1, "/teams"), 194 | concat!("/teams/", $p1), 195 | concat!("/teams/", $p1, "/members"), 196 | concat!("/teams/", $p1, "/members/", $p2), 197 | concat!("/teams/", $p1, "/repos"), 198 | concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), 199 | concat!("/user/teams"), 200 | concat!("/repos/", $p1, "/", $p2, "/pulls"), 201 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), 202 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), 203 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), 204 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), 205 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), 206 | concat!("/user/repos"), 207 | concat!("/users/", $p1, "/repos"), 208 | concat!("/orgs/", $p1, "/repos"), 209 | concat!("/repositories"), 210 | concat!("/repos/", $p1, "/", $p2), 211 | concat!("/repos/", $p1, "/", $p2, "/contributors"), 212 | concat!("/repos/", $p1, "/", $p2, "/languages"), 213 | concat!("/repos/", $p1, "/", $p2, "/teams"), 214 | concat!("/repos/", $p1, "/", $p2, "/tags"), 215 | concat!("/repos/", $p1, "/", $p2, "/branches"), 216 | concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), 217 | concat!("/repos/", $p1, "/", $p2, "/collaborators"), 218 | concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), 219 | concat!("/repos/", $p1, "/", $p2, "/comments"), 220 | concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), 221 | concat!("/repos/", $p1, "/", $p2, "/commits"), 222 | concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), 223 | concat!("/repos/", $p1, "/", $p2, "/readme"), 224 | concat!("/repos/", $p1, "/", $p2, "/keys"), 225 | concat!("/repos/", $p1, "/", $p2, "/keys/", $p3), 226 | concat!("/repos/", $p1, "/", $p2, "/downloads"), 227 | concat!("/repos/", $p1, "/", $p2, "/downloads/", $p3), 228 | concat!("/repos/", $p1, "/", $p2, "/forks"), 229 | concat!("/repos/", $p1, "/", $p2, "/hooks"), 230 | concat!("/repos/", $p1, "/", $p2, "/hooks/", $p3), 231 | concat!("/repos/", $p1, "/", $p2, "/releases"), 232 | concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), 233 | concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), 234 | concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), 235 | concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), 236 | concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), 237 | concat!("/repos/", $p1, "/", $p2, "/stats/participation"), 238 | concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), 239 | concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), 240 | concat!("/search/repositories"), 241 | concat!("/search/code"), 242 | concat!("/search/issues"), 243 | concat!("/search/users"), 244 | concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), 245 | concat!("/legacy/repos/search/", $p1), 246 | concat!("/legacy/user/search/", $p1), 247 | concat!("/legacy/user/email/", $p1), 248 | concat!("/users/", $p1), 249 | concat!("/user"), 250 | concat!("/users"), 251 | concat!("/user/emails"), 252 | concat!("/users/", $p1, "/followers"), 253 | concat!("/user/followers"), 254 | concat!("/users/", $p1, "/following"), 255 | concat!("/user/following"), 256 | concat!("/user/following/", $p1), 257 | concat!("/users/", $p1, "/following/", $p2), 258 | concat!("/users/", $p1, "/keys"), 259 | concat!("/user/keys"), 260 | concat!("/user/keys/", $p1), 261 | ] 262 | }}; 263 | } 264 | 265 | use routes; 266 | -------------------------------------------------------------------------------- /examples/hyper.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::future; 3 | use std::sync::Arc; 4 | 5 | use http_body_util::Full; 6 | use hyper::body::{Bytes, Incoming}; 7 | use hyper::server::conn::http1::Builder as ConnectionBuilder; 8 | use hyper::{Method, Request, Response, StatusCode}; 9 | use hyper_util::rt::TokioIo; 10 | use tokio::net::TcpListener; 11 | use tower::service_fn; 12 | use tower::Service as _; 13 | 14 | type Body = Full; 15 | 16 | // GET / 17 | async fn index(_req: Request) -> hyper::Result> { 18 | Ok(Response::new(Body::from("Hello, world!"))) 19 | } 20 | 21 | // GET /blog 22 | async fn blog(_req: Request) -> hyper::Result> { 23 | Ok(Response::new(Body::from("..."))) 24 | } 25 | 26 | // 404 handler 27 | async fn not_found(_req: Request) -> hyper::Result> { 28 | Ok(Response::builder() 29 | .status(StatusCode::NOT_FOUND) 30 | .body(Body::default()) 31 | .unwrap()) 32 | } 33 | 34 | // We can use `BoxCloneSyncService` to erase the type of each handler service. 35 | type Service = tower::util::BoxCloneSyncService, Response, hyper::Error>; 36 | 37 | // We use a `HashMap` to hold a `Router` for each HTTP method. This allows us 38 | // to register the same route for multiple methods. 39 | type Router = HashMap>; 40 | 41 | async fn route(router: Arc, req: Request) -> hyper::Result> { 42 | // find the subrouter for this request method 43 | let Some(router) = router.get(req.method()) else { 44 | // if there are no routes for this method, respond with 405 Method Not Allowed 45 | return Ok(Response::builder() 46 | .status(StatusCode::METHOD_NOT_ALLOWED) 47 | .body(Body::default()) 48 | .unwrap()); 49 | }; 50 | 51 | // find the service for this request path 52 | let Ok(found) = router.at(req.uri().path()) else { 53 | // if we there is no matching service, call the 404 handler 54 | return not_found(req).await; 55 | }; 56 | 57 | let mut service = found.value.clone(); 58 | 59 | future::poll_fn(|cx| service.poll_ready(cx)).await?; 60 | 61 | service.call(req).await 62 | } 63 | 64 | #[tokio::main] 65 | async fn main() { 66 | // Create a router and register our routes. 67 | let mut router = Router::new(); 68 | 69 | // GET / => `index` 70 | router 71 | .entry(Method::GET) 72 | .or_default() 73 | .insert("/", Service::new(service_fn(index))) 74 | .unwrap(); 75 | 76 | // GET /blog => `blog` 77 | router 78 | .entry(Method::GET) 79 | .or_default() 80 | .insert("/blog", Service::new(service_fn(blog))) 81 | .unwrap(); 82 | 83 | let listener = TcpListener::bind(("127.0.0.1", 3000)).await.unwrap(); 84 | 85 | // boilerplate for the hyper service 86 | let router = Arc::new(router); 87 | 88 | loop { 89 | let router = router.clone(); 90 | let (tcp, _) = listener.accept().await.unwrap(); 91 | tokio::task::spawn(async move { 92 | if let Err(err) = ConnectionBuilder::new() 93 | .serve_connection( 94 | TokioIo::new(tcp), 95 | hyper::service::service_fn(|request| route(router.clone(), request)), 96 | ) 97 | .await 98 | { 99 | println!("Error serving connection: {:?}", err); 100 | } 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "matchit-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.matchit] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "insert_and_match" 24 | path = "fuzz_targets/insert_and_match.rs" 25 | test = false 26 | doc = false 27 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/insert_and_match.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: (Vec<(String, i32)>, String, Option)| { 5 | let mut matcher = matchit::Node::new(); 6 | 7 | for (key, item) in data.0 { 8 | if matcher.insert(key, item).is_err() { 9 | return; 10 | } 11 | } 12 | 13 | match data.2 { 14 | None => { 15 | let _ = matcher.at(&data.1); 16 | } 17 | Some(b) => { 18 | let _ = matcher.path_ignore_case(&data.1, b); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::escape::{UnescapedRef, UnescapedRoute}; 2 | use crate::tree::{denormalize_params, Node}; 3 | 4 | use std::fmt; 5 | use std::ops::Deref; 6 | 7 | /// Represents errors that can occur when inserting a new route. 8 | #[non_exhaustive] 9 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 10 | pub enum InsertError { 11 | /// Attempted to insert a path that conflicts with an existing route. 12 | Conflict { 13 | /// The existing route that the insertion is conflicting with. 14 | with: String, 15 | }, 16 | 17 | /// Only one parameter per route segment is allowed. 18 | /// 19 | /// For example, `/foo-{bar}` and `/{bar}-foo` are valid routes, but `/{foo}-{bar}` 20 | /// is not. 21 | InvalidParamSegment, 22 | 23 | /// Parameters must be registered with a valid name and matching braces. 24 | /// 25 | /// Note you can use `{{` or `}}` to escape literal brackets. 26 | InvalidParam, 27 | 28 | /// Catch-all parameters are only allowed at the end of a path. 29 | InvalidCatchAll, 30 | } 31 | 32 | impl fmt::Display for InsertError { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | match self { 35 | Self::Conflict { with } => { 36 | write!( 37 | f, 38 | "Insertion failed due to conflict with previously registered route: {with}" 39 | ) 40 | } 41 | Self::InvalidParamSegment => { 42 | write!(f, "Only one parameter is allowed per path segment") 43 | } 44 | Self::InvalidParam => write!(f, "Parameters must be registered with a valid name"), 45 | Self::InvalidCatchAll => write!( 46 | f, 47 | "Catch-all parameters are only allowed at the end of a route" 48 | ), 49 | } 50 | } 51 | } 52 | 53 | impl std::error::Error for InsertError {} 54 | 55 | impl InsertError { 56 | /// Returns an error for a route conflict with the given node. 57 | /// 58 | /// This method attempts to find the full conflicting route. 59 | pub(crate) fn conflict( 60 | route: &UnescapedRoute, 61 | prefix: UnescapedRef<'_>, 62 | current: &Node, 63 | ) -> Self { 64 | let mut route = route.clone(); 65 | 66 | // The route is conflicting with the current node. 67 | if prefix.unescaped() == current.prefix.unescaped() { 68 | denormalize_params(&mut route, ¤t.remapping); 69 | return InsertError::Conflict { 70 | with: String::from_utf8(route.into_unescaped()).unwrap(), 71 | }; 72 | } 73 | 74 | // Remove the non-matching suffix from the route. 75 | route.truncate(route.len() - prefix.len()); 76 | 77 | // Add the conflicting prefix. 78 | if !route.ends_with(¤t.prefix) { 79 | route.append(¤t.prefix); 80 | } 81 | 82 | // Add the prefixes of any conflicting children. 83 | let mut child = current.children.first(); 84 | while let Some(node) = child { 85 | route.append(&node.prefix); 86 | child = node.children.first(); 87 | } 88 | 89 | // Denormalize any route parameters. 90 | let mut last = current; 91 | while let Some(node) = last.children.first() { 92 | last = node; 93 | } 94 | denormalize_params(&mut route, &last.remapping); 95 | 96 | // Return the conflicting route. 97 | InsertError::Conflict { 98 | with: String::from_utf8(route.into_unescaped()).unwrap(), 99 | } 100 | } 101 | } 102 | 103 | /// A failed merge attempt. 104 | /// 105 | /// See [`Router::merge`](crate::Router::merge) for details. 106 | #[derive(Clone, Debug, Eq, PartialEq)] 107 | pub struct MergeError(pub(crate) Vec); 108 | 109 | impl MergeError { 110 | /// Returns a list of [`InsertError`] for every insertion that failed 111 | /// during the merge. 112 | pub fn into_errors(self) -> Vec { 113 | self.0 114 | } 115 | } 116 | 117 | impl fmt::Display for MergeError { 118 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 119 | for error in self.0.iter() { 120 | writeln!(f, "{}", error)?; 121 | } 122 | 123 | Ok(()) 124 | } 125 | } 126 | 127 | impl std::error::Error for MergeError {} 128 | 129 | impl Deref for MergeError { 130 | type Target = Vec; 131 | 132 | fn deref(&self) -> &Self::Target { 133 | &self.0 134 | } 135 | } 136 | 137 | /// A failed match attempt. 138 | /// 139 | /// ``` 140 | /// use matchit::{MatchError, Router}; 141 | /// # fn main() -> Result<(), Box> { 142 | /// let mut router = Router::new(); 143 | /// router.insert("/home", "Welcome!")?; 144 | /// router.insert("/blog", "Our blog.")?; 145 | /// 146 | /// // no routes match 147 | /// if let Err(err) = router.at("/blo") { 148 | /// assert_eq!(err, MatchError::NotFound); 149 | /// } 150 | /// # Ok(()) 151 | /// # } 152 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 153 | pub enum MatchError { 154 | /// No matching route was found. 155 | NotFound, 156 | } 157 | 158 | impl fmt::Display for MatchError { 159 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 160 | write!(f, "Matching route not found") 161 | } 162 | } 163 | 164 | impl std::error::Error for MatchError {} 165 | -------------------------------------------------------------------------------- /src/escape.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, ops::Range}; 2 | 3 | /// An unescaped route that keeps track of the position of 4 | /// escaped characters, i.e. '{{' or '}}'. 5 | /// 6 | /// Note that this type dereferences to `&[u8]`. 7 | #[derive(Clone, Default)] 8 | pub struct UnescapedRoute { 9 | // The raw unescaped route. 10 | inner: Vec, 11 | escaped: Vec, 12 | } 13 | 14 | impl UnescapedRoute { 15 | /// Unescapes escaped brackets ('{{' or '}}') in a route. 16 | pub fn new(mut inner: Vec) -> UnescapedRoute { 17 | let mut escaped = Vec::new(); 18 | let mut i = 0; 19 | 20 | while let Some(&c) = inner.get(i) { 21 | if (c == b'{' && inner.get(i + 1) == Some(&b'{')) 22 | || (c == b'}' && inner.get(i + 1) == Some(&b'}')) 23 | { 24 | inner.remove(i); 25 | escaped.push(i); 26 | } 27 | 28 | i += 1; 29 | } 30 | 31 | UnescapedRoute { inner, escaped } 32 | } 33 | 34 | /// Returns true if the character at the given index was escaped. 35 | pub fn is_escaped(&self, i: usize) -> bool { 36 | self.escaped.contains(&i) 37 | } 38 | 39 | /// Replaces the characters in the given range. 40 | pub fn splice( 41 | &mut self, 42 | range: Range, 43 | replace: Vec, 44 | ) -> impl Iterator + '_ { 45 | // Ignore any escaped characters in the range being replaced. 46 | self.escaped.retain(|x| !range.contains(x)); 47 | 48 | // Update the escaped indices. 49 | let offset = (replace.len() as isize) - (range.len() as isize); 50 | for i in &mut self.escaped { 51 | if *i > range.end { 52 | *i = i.checked_add_signed(offset).unwrap(); 53 | } 54 | } 55 | 56 | self.inner.splice(range, replace) 57 | } 58 | 59 | /// Appends another route to the end of this one. 60 | pub fn append(&mut self, other: &UnescapedRoute) { 61 | for i in &other.escaped { 62 | self.escaped.push(self.inner.len() + i); 63 | } 64 | 65 | self.inner.extend_from_slice(&other.inner); 66 | } 67 | 68 | /// Truncates the route to the given length. 69 | pub fn truncate(&mut self, to: usize) { 70 | self.escaped.retain(|&x| x < to); 71 | self.inner.truncate(to); 72 | } 73 | 74 | /// Returns a reference to this route. 75 | pub fn as_ref(&self) -> UnescapedRef<'_> { 76 | UnescapedRef { 77 | inner: &self.inner, 78 | escaped: &self.escaped, 79 | offset: 0, 80 | } 81 | } 82 | 83 | /// Returns a reference to the unescaped slice. 84 | pub fn unescaped(&self) -> &[u8] { 85 | &self.inner 86 | } 87 | 88 | /// Returns the unescaped route. 89 | pub fn into_unescaped(self) -> Vec { 90 | self.inner 91 | } 92 | } 93 | 94 | impl std::ops::Deref for UnescapedRoute { 95 | type Target = [u8]; 96 | 97 | fn deref(&self) -> &Self::Target { 98 | &self.inner 99 | } 100 | } 101 | 102 | impl fmt::Debug for UnescapedRoute { 103 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 104 | fmt::Debug::fmt(std::str::from_utf8(&self.inner).unwrap(), f) 105 | } 106 | } 107 | 108 | /// A reference to an `UnescapedRoute`. 109 | #[derive(Copy, Clone)] 110 | pub struct UnescapedRef<'a> { 111 | inner: &'a [u8], 112 | escaped: &'a [usize], 113 | // An offset applied to each escaped index. 114 | offset: isize, 115 | } 116 | 117 | impl<'a> UnescapedRef<'a> { 118 | /// Converts this reference into an owned route. 119 | pub fn to_owned(self) -> UnescapedRoute { 120 | let mut escaped = Vec::new(); 121 | for &i in self.escaped { 122 | let i = i.checked_add_signed(self.offset); 123 | 124 | match i { 125 | Some(i) if i < self.inner.len() => escaped.push(i), 126 | _ => {} 127 | } 128 | } 129 | 130 | UnescapedRoute { 131 | escaped, 132 | inner: self.inner.to_owned(), 133 | } 134 | } 135 | 136 | /// Returns `true` if the character at the given index was escaped. 137 | pub fn is_escaped(&self, i: usize) -> bool { 138 | if let Some(i) = i.checked_add_signed(-self.offset) { 139 | return self.escaped.contains(&i); 140 | } 141 | 142 | false 143 | } 144 | 145 | /// Slices the route with `start..`. 146 | pub fn slice_off(&self, start: usize) -> UnescapedRef<'a> { 147 | UnescapedRef { 148 | inner: &self.inner[start..], 149 | escaped: self.escaped, 150 | offset: self.offset - (start as isize), 151 | } 152 | } 153 | 154 | /// Slices the route with `..end`. 155 | pub fn slice_until(&self, end: usize) -> UnescapedRef<'a> { 156 | UnescapedRef { 157 | inner: &self.inner[..end], 158 | escaped: self.escaped, 159 | offset: self.offset, 160 | } 161 | } 162 | 163 | /// Returns a reference to the unescaped slice. 164 | pub fn unescaped(&self) -> &[u8] { 165 | self.inner 166 | } 167 | } 168 | 169 | impl<'a> std::ops::Deref for UnescapedRef<'a> { 170 | type Target = &'a [u8]; 171 | 172 | fn deref(&self) -> &Self::Target { 173 | &self.inner 174 | } 175 | } 176 | 177 | impl fmt::Debug for UnescapedRef<'_> { 178 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 179 | f.debug_struct("UnescapedRef") 180 | .field("inner", &std::str::from_utf8(self.inner)) 181 | .field("escaped", &self.escaped) 182 | .field("offset", &self.offset) 183 | .finish() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | A high performance, zero-copy URL router. 3 | 4 | ```rust 5 | use matchit::Router; 6 | 7 | fn main() -> Result<(), Box> { 8 | let mut router = Router::new(); 9 | router.insert("/home", "Welcome!")?; 10 | router.insert("/users/{id}", "A User")?; 11 | 12 | let matched = router.at("/users/978")?; 13 | assert_eq!(matched.params.get("id"), Some("978")); 14 | assert_eq!(*matched.value, "A User"); 15 | 16 | Ok(()) 17 | } 18 | ``` 19 | 20 | # Parameters 21 | 22 | The router supports dynamic route segments. These can either be named or catch-all parameters. 23 | 24 | Named parameters like `/{id}` match anything until the next static segment or the end of the path. 25 | 26 | ```rust 27 | # use matchit::Router; 28 | # fn main() -> Result<(), Box> { 29 | let mut m = Router::new(); 30 | m.insert("/users/{id}", true)?; 31 | 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()); 35 | # Ok(()) 36 | # } 37 | ``` 38 | 39 | Prefixes and suffixes within a segment are also supported. However, there may only be a single named parameter per route segment. 40 | ```rust 41 | # use matchit::Router; 42 | # fn main() -> Result<(), Box> { 43 | let mut m = Router::new(); 44 | m.insert("/images/img{id}.png", true)?; 45 | 46 | assert_eq!(m.at("/images/img1.png")?.params.get("id"), Some("1")); 47 | assert!(m.at("/images/img1.jpg").is_err()); 48 | # Ok(()) 49 | # } 50 | ``` 51 | 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. 53 | 54 | ```rust 55 | # use matchit::Router; 56 | # fn main() -> Result<(), Box> { 57 | let mut m = Router::new(); 58 | m.insert("/{*p}", true)?; 59 | 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")); 62 | 63 | // Note that this would lead to an empty parameter. 64 | assert!(m.at("/").is_err()); 65 | # Ok(()) 66 | # } 67 | ``` 68 | 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 `}}`. 70 | 71 | ```rust 72 | # use matchit::Router; 73 | # fn main() -> Result<(), Box> { 74 | let mut m = Router::new(); 75 | m.insert("/{{hello}}", true)?; 76 | m.insert("/{hello}", true)?; 77 | 78 | // Match the static route. 79 | assert!(m.at("/{hello}")?.value); 80 | 81 | // Match the dynamic route. 82 | assert_eq!(m.at("/hello")?.params.get("hello"), Some("hello")); 83 | # Ok(()) 84 | # } 85 | ``` 86 | 87 | # Routing Priority 88 | 89 | Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority: 90 | 91 | ```rust 92 | # use matchit::Router; 93 | # fn main() -> Result<(), Box> { 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 98 | # Ok(()) 99 | # } 100 | ``` 101 | 102 | # How does it work? 103 | 104 | 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. 105 | 106 | ```text 107 | Priority Path Value 108 | 9 \ 1 109 | 3 ├s None 110 | 2 |├earch\ 2 111 | 1 |└upport\ 3 112 | 2 ├blog\ 4 113 | 1 | └{post} None 114 | 1 | └\ 5 115 | 2 ├about-us\ 6 116 | 1 | └team\ 7 117 | 1 └contact\ 8 118 | ``` 119 | 120 | This allows us to reduce the route search to a small number of branches. Child nodes on the same level of the tree are also prioritized 121 | by the number of children with registered values, increasing the chance of choosing the correct branch of the first try. 122 | 123 | As it turns out, this method of routing is extremely fast. See the [benchmark results](https://github.com/ibraheemdev/matchit?tab=readme-ov-file#benchmarks) for details. 124 | */ 125 | 126 | #![deny(rust_2018_idioms, clippy::all)] 127 | 128 | mod error; 129 | mod escape; 130 | mod params; 131 | mod router; 132 | mod tree; 133 | 134 | pub use error::{InsertError, MatchError, MergeError}; 135 | pub use params::{Params, ParamsIter}; 136 | pub use router::{Match, Router}; 137 | 138 | #[cfg(doctest)] 139 | mod readme { 140 | #[allow(dead_code)] 141 | #[doc = include_str!("../README.md")] 142 | struct Readme; 143 | } 144 | -------------------------------------------------------------------------------- /src/params.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, iter, mem, slice}; 2 | 3 | /// A single URL parameter, consisting of a key and a value. 4 | #[derive(PartialEq, Eq, Ord, PartialOrd, Default, Copy, Clone)] 5 | pub(crate) struct Param<'k, 'v> { 6 | // Keys and values are stored as byte slices internally by the router 7 | // to avoid utf8 checks when slicing. This allows us to perform utf8 8 | // validation lazily without resorting to unsafe code. 9 | pub(crate) key: &'k [u8], 10 | pub(crate) value: &'v [u8], 11 | } 12 | 13 | impl<'k, 'v> Param<'k, 'v> { 14 | const EMPTY: Param<'static, 'static> = Param { 15 | key: b"", 16 | value: b"", 17 | }; 18 | 19 | // Returns the parameter key as a string. 20 | fn key_str(&self) -> &'k str { 21 | std::str::from_utf8(self.key).unwrap() 22 | } 23 | 24 | // Returns the parameter value as a string. 25 | fn value_str(&self) -> &'v str { 26 | std::str::from_utf8(self.value).unwrap() 27 | } 28 | } 29 | 30 | /// A list of parameters returned by a route match. 31 | /// 32 | /// ```rust 33 | /// # fn main() -> Result<(), Box> { 34 | /// # let mut router = matchit::Router::new(); 35 | /// # router.insert("/users/{id}", true).unwrap(); 36 | /// let matched = router.at("/users/1")?; 37 | /// 38 | /// // Iterate through the keys and values. 39 | /// for (key, value) in matched.params.iter() { 40 | /// println!("key: {}, value: {}", key, value); 41 | /// } 42 | /// 43 | /// // Get a specific value by name. 44 | /// let id = matched.params.get("id"); 45 | /// assert_eq!(id, Some("1")); 46 | /// # Ok(()) 47 | /// # } 48 | /// ``` 49 | #[derive(PartialEq, Eq, Ord, PartialOrd, Clone)] 50 | pub struct Params<'k, 'v> { 51 | kind: ParamsKind<'k, 'v>, 52 | } 53 | 54 | impl Default for Params<'_, '_> { 55 | fn default() -> Self { 56 | Self::new() 57 | } 58 | } 59 | 60 | // Most routes have a small number of dynamic parameters, so we can avoid 61 | // heap allocations in the common case. 62 | const SMALL: usize = 3; 63 | 64 | // A list of parameters, optimized to avoid allocations when possible. 65 | #[derive(PartialEq, Eq, Ord, PartialOrd, Clone)] 66 | enum ParamsKind<'k, 'v> { 67 | Small([Param<'k, 'v>; SMALL], usize), 68 | Large(Vec>), 69 | } 70 | 71 | impl<'k, 'v> Params<'k, 'v> { 72 | /// Create an empty list of parameters. 73 | #[inline] 74 | pub fn new() -> Self { 75 | Self { 76 | kind: ParamsKind::Small([Param::EMPTY; SMALL], 0), 77 | } 78 | } 79 | 80 | /// Returns the number of parameters. 81 | pub fn len(&self) -> usize { 82 | match self.kind { 83 | ParamsKind::Small(_, len) => len, 84 | ParamsKind::Large(ref vec) => vec.len(), 85 | } 86 | } 87 | 88 | // Truncates the parameter list to the given length. 89 | pub(crate) fn truncate(&mut self, n: usize) { 90 | match &mut self.kind { 91 | ParamsKind::Small(_, len) => *len = n, 92 | ParamsKind::Large(vec) => vec.truncate(n), 93 | } 94 | } 95 | 96 | /// Returns the value of the first parameter registered under the given key. 97 | pub fn get(&self, key: impl AsRef) -> Option<&'v str> { 98 | let key = key.as_ref().as_bytes(); 99 | 100 | match &self.kind { 101 | ParamsKind::Small(arr, len) => arr 102 | .iter() 103 | .take(*len) 104 | .find(|param| param.key == key) 105 | .map(Param::value_str), 106 | ParamsKind::Large(vec) => vec 107 | .iter() 108 | .find(|param| param.key == key) 109 | .map(Param::value_str), 110 | } 111 | } 112 | 113 | /// Returns an iterator over the parameters in the list. 114 | pub fn iter(&self) -> ParamsIter<'_, 'k, 'v> { 115 | ParamsIter::new(self) 116 | } 117 | 118 | /// Returns `true` if there are no parameters in the list. 119 | pub fn is_empty(&self) -> bool { 120 | match self.kind { 121 | ParamsKind::Small(_, len) => len == 0, 122 | ParamsKind::Large(ref vec) => vec.is_empty(), 123 | } 124 | } 125 | 126 | /// Appends a key-value parameter to the list. 127 | #[inline] 128 | pub(crate) fn push(&mut self, key: &'k [u8], value: &'v [u8]) { 129 | #[cold] 130 | #[inline(never)] 131 | fn drain_to_vec(len: usize, elem: T, arr: &mut [T; SMALL]) -> Vec { 132 | let mut vec = Vec::with_capacity(len + 1); 133 | vec.extend(arr.iter_mut().map(mem::take)); 134 | vec.push(elem); 135 | vec 136 | } 137 | 138 | #[cold] 139 | #[inline(never)] 140 | fn push_slow<'k, 'v>(vec: &mut Vec>, param: Param<'k, 'v>) { 141 | vec.push(param); 142 | } 143 | 144 | let param = Param { key, value }; 145 | match &mut self.kind { 146 | ParamsKind::Small(arr, len) => { 147 | if *len >= SMALL { 148 | self.kind = ParamsKind::Large(drain_to_vec(*len, param, arr)); 149 | return; 150 | } 151 | 152 | arr[*len] = param; 153 | *len += 1; 154 | } 155 | 156 | ParamsKind::Large(vec) => push_slow(vec, param), 157 | } 158 | } 159 | 160 | // Applies a transformation function to each key. 161 | #[inline] 162 | pub(crate) fn for_each_key_mut(&mut self, f: impl Fn((usize, &mut Param<'k, 'v>))) { 163 | match &mut self.kind { 164 | ParamsKind::Small(arr, len) => arr.iter_mut().take(*len).enumerate().for_each(f), 165 | ParamsKind::Large(vec) => vec.iter_mut().enumerate().for_each(f), 166 | } 167 | } 168 | } 169 | 170 | impl fmt::Debug for Params<'_, '_> { 171 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 172 | f.debug_list().entries(self.iter()).finish() 173 | } 174 | } 175 | 176 | /// An iterator over the keys and values of a route's [parameters](crate::Params). 177 | pub struct ParamsIter<'ps, 'k, 'v> { 178 | kind: ParamsIterKind<'ps, 'k, 'v>, 179 | } 180 | 181 | impl<'ps, 'k, 'v> ParamsIter<'ps, 'k, 'v> { 182 | fn new(params: &'ps Params<'k, 'v>) -> Self { 183 | let kind = match ¶ms.kind { 184 | ParamsKind::Small(arr, len) => ParamsIterKind::Small(arr.iter().take(*len)), 185 | ParamsKind::Large(vec) => ParamsIterKind::Large(vec.iter()), 186 | }; 187 | Self { kind } 188 | } 189 | } 190 | 191 | enum ParamsIterKind<'ps, 'k, 'v> { 192 | Small(iter::Take>>), 193 | Large(slice::Iter<'ps, Param<'k, 'v>>), 194 | } 195 | 196 | impl<'k, 'v> Iterator for ParamsIter<'_, 'k, 'v> { 197 | type Item = (&'k str, &'v str); 198 | 199 | fn next(&mut self) -> Option { 200 | match self.kind { 201 | ParamsIterKind::Small(ref mut iter) => { 202 | iter.next().map(|p| (p.key_str(), p.value_str())) 203 | } 204 | ParamsIterKind::Large(ref mut iter) => { 205 | iter.next().map(|p| (p.key_str(), p.value_str())) 206 | } 207 | } 208 | } 209 | } 210 | 211 | impl ExactSizeIterator for ParamsIter<'_, '_, '_> { 212 | fn len(&self) -> usize { 213 | match self.kind { 214 | ParamsIterKind::Small(ref iter) => iter.len(), 215 | ParamsIterKind::Large(ref iter) => iter.len(), 216 | } 217 | } 218 | } 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use super::*; 223 | 224 | #[test] 225 | fn heap_alloc() { 226 | let vec = vec![ 227 | ("hello", "hello"), 228 | ("world", "world"), 229 | ("foo", "foo"), 230 | ("bar", "bar"), 231 | ("baz", "baz"), 232 | ]; 233 | 234 | let mut params = Params::new(); 235 | for (key, value) in vec.clone() { 236 | params.push(key.as_bytes(), value.as_bytes()); 237 | assert_eq!(params.get(key), Some(value)); 238 | } 239 | 240 | match params.kind { 241 | ParamsKind::Large(..) => {} 242 | _ => panic!(), 243 | } 244 | 245 | assert!(params.iter().eq(vec.clone())); 246 | } 247 | 248 | #[test] 249 | fn stack_alloc() { 250 | let vec = vec![("hello", "hello"), ("world", "world"), ("baz", "baz")]; 251 | 252 | let mut params = Params::new(); 253 | for (key, value) in vec.clone() { 254 | params.push(key.as_bytes(), value.as_bytes()); 255 | assert_eq!(params.get(key), Some(value)); 256 | } 257 | 258 | match params.kind { 259 | ParamsKind::Small(..) => {} 260 | _ => panic!(), 261 | } 262 | 263 | assert!(params.iter().eq(vec.clone())); 264 | } 265 | 266 | #[test] 267 | fn ignore_array_default() { 268 | let params = Params::new(); 269 | assert!(params.get("").is_none()); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/router.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MergeError; 2 | use crate::tree::Node; 3 | use crate::{InsertError, MatchError, Params}; 4 | 5 | /// A zero-copy URL router. 6 | /// 7 | /// See [the crate documentation](crate) for details. 8 | #[derive(Clone, Debug)] 9 | pub struct Router { 10 | root: Node, 11 | } 12 | 13 | impl Default for Router { 14 | fn default() -> Self { 15 | Self { 16 | root: Node::default(), 17 | } 18 | } 19 | } 20 | 21 | impl Router { 22 | /// Construct a new router. 23 | pub fn new() -> Self { 24 | Self::default() 25 | } 26 | 27 | /// Insert a route into the router. 28 | /// 29 | /// # Examples 30 | /// 31 | /// ```rust 32 | /// # use matchit::Router; 33 | /// # fn main() -> Result<(), Box> { 34 | /// let mut router = Router::new(); 35 | /// router.insert("/home", "Welcome!")?; 36 | /// router.insert("/users/{id}", "A User")?; 37 | /// # Ok(()) 38 | /// # } 39 | /// ``` 40 | pub fn insert(&mut self, route: impl Into, value: T) -> Result<(), InsertError> { 41 | self.root.insert(route.into(), value) 42 | } 43 | 44 | /// Tries to find a value in the router matching the given path. 45 | /// 46 | /// # Examples 47 | /// 48 | /// ```rust 49 | /// # use matchit::Router; 50 | /// # fn main() -> Result<(), Box> { 51 | /// let mut router = Router::new(); 52 | /// router.insert("/home", "Welcome!")?; 53 | /// 54 | /// let matched = router.at("/home").unwrap(); 55 | /// assert_eq!(*matched.value, "Welcome!"); 56 | /// # Ok(()) 57 | /// # } 58 | /// ``` 59 | #[inline] 60 | pub fn at<'path>(&self, path: &'path str) -> Result, MatchError> { 61 | match self.root.at(path.as_bytes()) { 62 | Ok((value, params)) => Ok(Match { 63 | // Safety: We only expose `&mut T` through `&mut self` 64 | value: unsafe { &*value.get() }, 65 | params, 66 | }), 67 | Err(e) => Err(e), 68 | } 69 | } 70 | 71 | /// Tries to find a value in the router matching the given path, 72 | /// returning a mutable reference. 73 | /// 74 | /// # Examples 75 | /// 76 | /// ```rust 77 | /// # use matchit::Router; 78 | /// # fn main() -> Result<(), Box> { 79 | /// let mut router = Router::new(); 80 | /// router.insert("/", 1)?; 81 | /// 82 | /// *router.at_mut("/").unwrap().value += 1; 83 | /// assert_eq!(*router.at("/").unwrap().value, 2); 84 | /// # Ok(()) 85 | /// # } 86 | /// ``` 87 | #[inline] 88 | pub fn at_mut<'path>( 89 | &mut self, 90 | path: &'path str, 91 | ) -> Result, MatchError> { 92 | match self.root.at(path.as_bytes()) { 93 | Ok((value, params)) => Ok(Match { 94 | // Safety: We have `&mut self` 95 | value: unsafe { &mut *value.get() }, 96 | params, 97 | }), 98 | Err(e) => Err(e), 99 | } 100 | } 101 | 102 | /// Remove a given route from the router. 103 | /// 104 | /// Returns the value stored under the route if it was found. 105 | /// If the route was not found or invalid, `None` is returned. 106 | /// 107 | /// # Examples 108 | /// 109 | /// ```rust 110 | /// # use matchit::Router; 111 | /// let mut router = Router::new(); 112 | /// 113 | /// router.insert("/home", "Welcome!"); 114 | /// assert_eq!(router.remove("/home"), Some("Welcome!")); 115 | /// assert_eq!(router.remove("/home"), None); 116 | /// 117 | /// router.insert("/home/{id}/", "Hello!"); 118 | /// assert_eq!(router.remove("/home/{id}/"), Some("Hello!")); 119 | /// assert_eq!(router.remove("/home/{id}/"), None); 120 | /// 121 | /// router.insert("/home/{id}/", "Hello!"); 122 | /// // The route does not match. 123 | /// assert_eq!(router.remove("/home/{user}"), None); 124 | /// assert_eq!(router.remove("/home/{id}/"), Some("Hello!")); 125 | /// 126 | /// router.insert("/home/{id}/", "Hello!"); 127 | /// // Invalid route. 128 | /// assert_eq!(router.remove("/home/{id"), None); 129 | /// assert_eq!(router.remove("/home/{id}/"), Some("Hello!")); 130 | /// ``` 131 | pub fn remove(&mut self, path: impl Into) -> Option { 132 | self.root.remove(path.into()) 133 | } 134 | 135 | #[cfg(feature = "__test_helpers")] 136 | pub fn check_priorities(&self) -> Result { 137 | self.root.check_priorities() 138 | } 139 | 140 | /// Merge a given router into current one. 141 | /// 142 | /// Returns a list of [`InsertError`] for every failed insertion. 143 | /// Note that this can result in a partially successful merge if 144 | /// a subset of routes conflict. 145 | /// 146 | /// # Examples 147 | /// 148 | /// ```rust 149 | /// # use matchit::Router; 150 | /// # fn main() -> Result<(), Box> { 151 | /// let mut root = Router::new(); 152 | /// root.insert("/home", "Welcome!")?; 153 | /// 154 | /// let mut child = Router::new(); 155 | /// child.insert("/users/{id}", "A User")?; 156 | /// 157 | /// root.merge(child)?; 158 | /// assert!(root.at("/users/1").is_ok()); 159 | /// # Ok(()) 160 | /// # } 161 | /// ``` 162 | pub fn merge(&mut self, other: Self) -> Result<(), MergeError> { 163 | let mut errors = Vec::new(); 164 | other.root.for_each(|path, value| { 165 | if let Err(err) = self.insert(path, value) { 166 | errors.push(err); 167 | } 168 | }); 169 | 170 | if errors.is_empty() { 171 | Ok(()) 172 | } else { 173 | Err(MergeError(errors)) 174 | } 175 | } 176 | } 177 | 178 | /// A successful match consisting of the registered value 179 | /// and URL parameters, returned by [`Router::at`](Router::at). 180 | #[derive(Debug)] 181 | pub struct Match<'k, 'v, V> { 182 | /// The value stored under the matched node. 183 | pub value: V, 184 | 185 | /// The route parameters. See [parameters](crate#parameters) for more details. 186 | pub params: Params<'k, 'v>, 187 | } 188 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::escape::{UnescapedRef, UnescapedRoute}; 2 | use crate::{InsertError, MatchError, Params}; 3 | 4 | use std::cell::UnsafeCell; 5 | use std::cmp::min; 6 | use std::collections::VecDeque; 7 | use std::ops::Range; 8 | use std::{fmt, mem}; 9 | 10 | /// A radix tree used for URL path matching. 11 | /// 12 | /// See [the crate documentation](crate) for details. 13 | pub struct Node { 14 | // This node's prefix. 15 | pub(crate) prefix: UnescapedRoute, 16 | 17 | // The priority of this node. 18 | // 19 | // Nodes with more children are higher priority and searched first. 20 | pub(crate) priority: u32, 21 | 22 | // Whether this node contains a wildcard child. 23 | pub(crate) wild_child: bool, 24 | 25 | // The first character of any static children, for fast linear search. 26 | pub(crate) indices: Vec, 27 | 28 | // The type of this node. 29 | pub(crate) node_type: NodeType, 30 | 31 | // The children of this node. 32 | pub(crate) children: Vec>, 33 | 34 | // The value stored at this node. 35 | // 36 | // See `Node::at` for why an `UnsafeCell` is necessary. 37 | value: Option>, 38 | 39 | // A parameter name remapping, stored at nodes that hold values. 40 | pub(crate) remapping: ParamRemapping, 41 | } 42 | 43 | /// The types of nodes a tree can hold. 44 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] 45 | pub(crate) enum NodeType { 46 | /// The root path. 47 | Root, 48 | 49 | /// A route parameter, e.g. '/{id}'. 50 | /// 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 }, 61 | 62 | /// A catch-all parameter, e.g. '/{*file}'. 63 | CatchAll, 64 | 65 | /// A static prefix, e.g. '/foo'. 66 | Static, 67 | } 68 | 69 | /// Safety: We expose `value` per Rust's usual borrowing rules, so we can just 70 | /// delegate these traits. 71 | unsafe impl Send for Node {} 72 | unsafe impl Sync for Node {} 73 | 74 | impl Node { 75 | // Insert a route into the tree. 76 | pub fn insert(&mut self, route: String, val: T) -> Result<(), InsertError> { 77 | let route = UnescapedRoute::new(route.into_bytes()); 78 | let (route, remapping) = normalize_params(route)?; 79 | let mut remaining = route.as_ref(); 80 | 81 | self.priority += 1; 82 | 83 | // If the tree is empty, insert the root node. 84 | if self.prefix.is_empty() && self.children.is_empty() { 85 | let last = self.insert_route(remaining, val)?; 86 | last.remapping = remapping; 87 | self.node_type = NodeType::Root; 88 | return Ok(()); 89 | } 90 | 91 | let mut node = self; 92 | 'walk: loop { 93 | // Find the common prefix between the route and the current node. 94 | let len = min(remaining.len(), node.prefix.len()); 95 | let common_prefix = (0..len) 96 | .find(|&i| { 97 | remaining[i] != node.prefix[i] 98 | // Make sure not confuse the start of a wildcard with an escaped `{`. 99 | || remaining.is_escaped(i) != node.prefix.is_escaped(i) 100 | }) 101 | .unwrap_or(len); 102 | 103 | // If this node has a longer prefix than we need, we have to fork and extract the 104 | // common prefix into a shared parent. 105 | if node.prefix.len() > common_prefix { 106 | // Move the non-matching suffix into a child node. 107 | let suffix = node.prefix.as_ref().slice_off(common_prefix).to_owned(); 108 | let child = Node { 109 | prefix: suffix, 110 | value: node.value.take(), 111 | indices: node.indices.clone(), 112 | wild_child: node.wild_child, 113 | children: mem::take(&mut node.children), 114 | remapping: mem::take(&mut node.remapping), 115 | priority: node.priority - 1, 116 | node_type: NodeType::Static, 117 | }; 118 | 119 | // The current node now only holds the common prefix. 120 | node.children = vec![child]; 121 | node.indices = vec![node.prefix[common_prefix]]; 122 | node.prefix = node.prefix.as_ref().slice_until(common_prefix).to_owned(); 123 | node.wild_child = false; 124 | continue; 125 | } 126 | 127 | if remaining.len() == common_prefix { 128 | // This node must not already contain a value. 129 | if node.value.is_some() { 130 | return Err(InsertError::conflict(&route, remaining, node)); 131 | } 132 | 133 | // Insert the value. 134 | node.value = Some(UnsafeCell::new(val)); 135 | node.remapping = remapping; 136 | return Ok(()); 137 | } 138 | 139 | // Otherwise, the route has a remaining non-matching suffix. 140 | // 141 | // We have to search deeper. 142 | remaining = remaining.slice_off(common_prefix); 143 | let next = remaining[0]; 144 | 145 | // For parameters with a suffix, we have to find the matching suffix or 146 | // create a new child node. 147 | if matches!(node.node_type, NodeType::Param { .. }) { 148 | let terminator = remaining 149 | .iter() 150 | .position(|&b| b == b'/') 151 | .map(|b| b + 1) 152 | .unwrap_or(remaining.len()); 153 | 154 | let suffix = remaining.slice_until(terminator); 155 | 156 | for (i, child) in node.children.iter().enumerate() { 157 | // Find a matching suffix. 158 | if *child.prefix == **suffix { 159 | node = &mut node.children[i]; 160 | node.priority += 1; 161 | continue 'walk; 162 | } 163 | } 164 | 165 | // Multiple parameters within the same segment, e.g. `/{foo}{bar}`. 166 | if matches!(find_wildcard(suffix), Ok(Some(_))) { 167 | return Err(InsertError::InvalidParamSegment); 168 | } 169 | 170 | // If there is no matching suffix, create a new suffix node. 171 | let child = node.add_suffix_child(Node { 172 | prefix: suffix.to_owned(), 173 | node_type: NodeType::Static, 174 | priority: 1, 175 | ..Node::default() 176 | }); 177 | node.node_type = NodeType::Param { suffix: true }; 178 | node = &mut node.children[child]; 179 | 180 | // If this is the final route segment, insert the value. 181 | if terminator == remaining.len() { 182 | node.value = Some(UnsafeCell::new(val)); 183 | node.remapping = remapping; 184 | return Ok(()); 185 | } 186 | 187 | // Otherwise, the previous node will hold only the suffix and we 188 | // need to create a new child for the remaining route. 189 | remaining = remaining.slice_off(terminator); 190 | 191 | // Create a static node unless we are inserting a parameter. 192 | if remaining[0] != b'{' || remaining.is_escaped(0) { 193 | let child = node.add_child(Node { 194 | node_type: NodeType::Static, 195 | priority: 1, 196 | ..Node::default() 197 | }); 198 | node.indices.push(remaining[0]); 199 | node = &mut node.children[child]; 200 | } 201 | 202 | // Insert the remaining route. 203 | let last = node.insert_route(remaining, val)?; 204 | last.remapping = remapping; 205 | return Ok(()); 206 | } 207 | 208 | // Find a child node that matches the next character in the route. 209 | for mut i in 0..node.indices.len() { 210 | if next == node.indices[i] { 211 | // Make sure not confuse the start of a wildcard with an escaped `{` or `}`. 212 | if matches!(next, b'{' | b'}') && !remaining.is_escaped(0) { 213 | continue; 214 | } 215 | 216 | // Continue searching in the child. 217 | i = node.update_child_priority(i); 218 | node = &mut node.children[i]; 219 | continue 'walk; 220 | } 221 | } 222 | 223 | // We couldn't find a matching child. 224 | // 225 | // If we're not inserting a wildcard we have to create a static child. 226 | if (next != b'{' || remaining.is_escaped(0)) && node.node_type != NodeType::CatchAll { 227 | node.indices.push(next); 228 | let child = node.add_child(Node::default()); 229 | let child = node.update_child_priority(child); 230 | 231 | // Insert into the newly created node. 232 | let last = node.children[child].insert_route(remaining, val)?; 233 | last.remapping = remapping; 234 | return Ok(()); 235 | } 236 | 237 | // We're trying to insert a wildcard. 238 | // 239 | // If this node already has a wildcard child, we have to make sure it matches. 240 | if node.wild_child { 241 | // Wildcards are always the last child. 242 | node = node.children.last_mut().unwrap(); 243 | node.priority += 1; 244 | 245 | // Make sure the route parameter matches. 246 | if let Some(wildcard) = remaining.get(..node.prefix.len()) { 247 | if *wildcard != *node.prefix { 248 | return Err(InsertError::conflict(&route, remaining, node)); 249 | } 250 | } 251 | 252 | // Catch-all routes cannot have children. 253 | if node.node_type == NodeType::CatchAll { 254 | return Err(InsertError::conflict(&route, remaining, node)); 255 | } 256 | 257 | // Continue with the wildcard node. 258 | continue 'walk; 259 | } 260 | 261 | // Otherwise, create a new node for the wildcard and insert the route. 262 | let last = node.insert_route(remaining, val)?; 263 | last.remapping = remapping; 264 | return Ok(()); 265 | } 266 | } 267 | 268 | // Insert a route at this node. 269 | // 270 | // If the route starts with a wildcard, a child node will be created for the parameter 271 | // and `wild_child` will be set on the parent. 272 | fn insert_route( 273 | &mut self, 274 | mut prefix: UnescapedRef<'_>, 275 | val: T, 276 | ) -> Result<&mut Node, InsertError> { 277 | let mut node = self; 278 | 279 | loop { 280 | // Search for a wildcard segment. 281 | let Some(wildcard) = find_wildcard(prefix)? else { 282 | // There is no wildcard, simply insert into the current node. 283 | node.value = Some(UnsafeCell::new(val)); 284 | node.prefix = prefix.to_owned(); 285 | return Ok(node); 286 | }; 287 | 288 | // Insering a catch-all route. 289 | if prefix[wildcard.clone()][1] == b'*' { 290 | // Ensure there is no suffix after the parameter, e.g. `/foo/{*x}/bar`. 291 | if wildcard.end != prefix.len() { 292 | return Err(InsertError::InvalidCatchAll); 293 | } 294 | 295 | // Add the prefix before the wildcard into the current node. 296 | if wildcard.start > 0 { 297 | node.prefix = prefix.slice_until(wildcard.start).to_owned(); 298 | prefix = prefix.slice_off(wildcard.start); 299 | } 300 | 301 | // Add the catch-all as a child node. 302 | let child = node.add_child(Node { 303 | prefix: prefix.to_owned(), 304 | node_type: NodeType::CatchAll, 305 | value: Some(UnsafeCell::new(val)), 306 | priority: 1, 307 | ..Node::default() 308 | }); 309 | node.wild_child = true; 310 | return Ok(&mut node.children[child]); 311 | } 312 | 313 | // Otherwise, we're inserting a regular route parameter. 314 | // 315 | // Add the prefix before the wildcard into the current node. 316 | if wildcard.start > 0 { 317 | node.prefix = prefix.slice_until(wildcard.start).to_owned(); 318 | prefix = prefix.slice_off(wildcard.start); 319 | } 320 | 321 | // Find the end of this route segment. 322 | let terminator = prefix 323 | .iter() 324 | .position(|&b| b == b'/') 325 | // Include the '/' in the suffix. 326 | .map(|b| b + 1) 327 | .unwrap_or(prefix.len()); 328 | 329 | let wildcard = prefix.slice_until(wildcard.len()); 330 | let suffix = prefix.slice_until(terminator).slice_off(wildcard.len()); 331 | prefix = prefix.slice_off(terminator); 332 | 333 | // Multiple parameters within the same segment, e.g. `/{foo}{bar}`. 334 | if matches!(find_wildcard(suffix), Ok(Some(_))) { 335 | return Err(InsertError::InvalidParamSegment); 336 | } 337 | 338 | // Add the parameter as a child node. 339 | let has_suffix = !matches!(*suffix, b"" | b"/"); 340 | let child = node.add_child(Node { 341 | priority: 1, 342 | node_type: NodeType::Param { suffix: has_suffix }, 343 | prefix: wildcard.to_owned(), 344 | ..Node::default() 345 | }); 346 | 347 | node.wild_child = true; 348 | node = &mut node.children[child]; 349 | 350 | // Add the static suffix until the '/', if there is one. 351 | // 352 | // Note that for '/' suffixes where `suffix: false`, this 353 | // unconditionally introduces an extra node for the '/' 354 | // without attempting to merge with the remaining route. 355 | // This makes converting a non-suffix parameter node into 356 | // a suffix one easier during insertion, but slightly hurts 357 | // performance. 358 | if !suffix.is_empty() { 359 | let child = node.add_suffix_child(Node { 360 | priority: 1, 361 | node_type: NodeType::Static, 362 | prefix: suffix.to_owned(), 363 | ..Node::default() 364 | }); 365 | 366 | node = &mut node.children[child]; 367 | } 368 | 369 | // If the route ends here, insert the value. 370 | if prefix.is_empty() { 371 | node.value = Some(UnsafeCell::new(val)); 372 | return Ok(node); 373 | } 374 | 375 | // If there is a static segment after the '/', setup the node 376 | // for the rest of the route. 377 | if prefix[0] != b'{' || prefix.is_escaped(0) { 378 | node.indices.push(prefix[0]); 379 | let child = node.add_child(Node { 380 | priority: 1, 381 | ..Node::default() 382 | }); 383 | node = &mut node.children[child]; 384 | } 385 | } 386 | } 387 | 388 | // Adds a child to this node, keeping wildcards at the end. 389 | fn add_child(&mut self, child: Node) -> usize { 390 | let len = self.children.len(); 391 | 392 | if self.wild_child && len > 0 { 393 | self.children.insert(len - 1, child); 394 | len - 1 395 | } else { 396 | self.children.push(child); 397 | len 398 | } 399 | } 400 | 401 | // Adds a suffix child to this node, keeping suffixes sorted by ascending length. 402 | fn add_suffix_child(&mut self, child: Node) -> usize { 403 | let i = self 404 | .children 405 | .partition_point(|node| node.prefix.len() >= child.prefix.len()); 406 | self.children.insert(i, child); 407 | i 408 | } 409 | 410 | // Increments priority of the given child node, reordering the children if necessary. 411 | // 412 | // Returns the new index of the node. 413 | fn update_child_priority(&mut self, i: usize) -> usize { 414 | self.children[i].priority += 1; 415 | let priority = self.children[i].priority; 416 | 417 | // Move the node to the front as necessary. 418 | let mut updated = i; 419 | while updated > 0 && self.children[updated - 1].priority < priority { 420 | self.children.swap(updated - 1, updated); 421 | updated -= 1; 422 | } 423 | 424 | // Update the position of the indices to match. 425 | if updated != i { 426 | self.indices[updated..=i].rotate_right(1); 427 | } 428 | 429 | updated 430 | } 431 | 432 | /// Removes a route from the tree, returning the value if the route already existed. 433 | /// 434 | /// The provided path should be the same as the one used to insert the route, including 435 | /// wildcards. 436 | pub fn remove(&mut self, route: String) -> Option { 437 | let route = UnescapedRoute::new(route.into_bytes()); 438 | let (route, remapping) = normalize_params(route).ok()?; 439 | let mut remaining = route.unescaped(); 440 | 441 | // Check if we are removing the root node. 442 | if remaining == self.prefix.unescaped() { 443 | let value = self.value.take().map(UnsafeCell::into_inner); 444 | 445 | // If the root node has no children, we can reset it. 446 | if self.children.is_empty() { 447 | *self = Node::default(); 448 | } 449 | 450 | return value; 451 | } 452 | 453 | let mut node = self; 454 | 'walk: loop { 455 | // Could not find a match. 456 | if remaining.len() <= node.prefix.len() { 457 | return None; 458 | } 459 | 460 | // Otherwise, the path is longer than this node's prefix, search deeper. 461 | let (prefix, rest) = remaining.split_at(node.prefix.len()); 462 | 463 | // The prefix does not match. 464 | if prefix != node.prefix.unescaped() { 465 | return None; 466 | } 467 | 468 | let next = rest[0]; 469 | remaining = rest; 470 | 471 | // If this is a parameter node, we have to find the matching suffix. 472 | if matches!(node.node_type, NodeType::Param { .. }) { 473 | let terminator = remaining 474 | .iter() 475 | .position(|&b| b == b'/') 476 | .map(|b| b + 1) 477 | .unwrap_or(remaining.len()); 478 | 479 | let suffix = &remaining[..terminator]; 480 | 481 | for (i, child) in node.children.iter().enumerate() { 482 | // Find the matching suffix. 483 | if *child.prefix == *suffix { 484 | // If this is the end of the path, remove the suffix node. 485 | if terminator == remaining.len() { 486 | return node.remove_child(i, &remapping); 487 | } 488 | 489 | // Otherwise, continue searching. 490 | remaining = &remaining[terminator - child.prefix.len()..]; 491 | node = &mut node.children[i]; 492 | continue 'walk; 493 | } 494 | } 495 | } 496 | 497 | // Find a child node that matches the next character in the route. 498 | if let Some(i) = node.indices.iter().position(|&c| c == next) { 499 | // The route matches, remove the node. 500 | if node.children[i].prefix.unescaped() == remaining { 501 | return node.remove_child(i, &remapping); 502 | } 503 | 504 | // Otherwise, continue searching. 505 | node = &mut node.children[i]; 506 | continue 'walk; 507 | } 508 | 509 | // If there is no matching wildcard child, there is no matching route. 510 | if !node.wild_child { 511 | return None; 512 | } 513 | 514 | // If the route does match, remove the node. 515 | if node.children.last_mut().unwrap().prefix.unescaped() == remaining { 516 | return node.remove_child(node.children.len() - 1, &remapping); 517 | } 518 | 519 | // Otherwise, keep searching deeper. 520 | node = node.children.last_mut().unwrap(); 521 | } 522 | } 523 | 524 | /// Remove the child node at the given index, if the route parameters match. 525 | fn remove_child(&mut self, i: usize, remapping: &ParamRemapping) -> Option { 526 | // Require an exact match to remove a route. 527 | // 528 | // For example, `/{a}` cannot be used to remove `/{b}`. 529 | if self.children[i].remapping != *remapping { 530 | return None; 531 | } 532 | 533 | // If the node does not have any children, we can remove it completely. 534 | let value = if self.children[i].children.is_empty() { 535 | // Remove the child node. 536 | let child = self.children.remove(i); 537 | 538 | match child.node_type { 539 | // Remove the index if we removed a static prefix that is 540 | // not a suffix node. 541 | NodeType::Static if !matches!(self.node_type, NodeType::Param { .. }) => { 542 | self.indices.remove(i); 543 | } 544 | 545 | // Otherwise, we removed a wildcard. 546 | _ => self.wild_child = false, 547 | } 548 | 549 | child.value 550 | } 551 | // Otherwise, remove the value but preserve the node. 552 | else { 553 | self.children[i].value.take() 554 | }; 555 | 556 | value.map(UnsafeCell::into_inner) 557 | } 558 | 559 | /// Iterates over the tree and calls the given visitor function 560 | /// with fully resolved path and its value. 561 | pub fn for_each(self, mut visitor: V) { 562 | let mut queue = VecDeque::from([(self.prefix.clone(), self)]); 563 | 564 | // Perform a BFS on the routing tree. 565 | while let Some((mut prefix, mut node)) = queue.pop_front() { 566 | denormalize_params(&mut prefix, &node.remapping); 567 | 568 | if let Some(value) = node.value.take() { 569 | let path = String::from_utf8(prefix.unescaped().to_vec()).unwrap(); 570 | visitor(path, value.into_inner()); 571 | } 572 | 573 | // Traverse the child nodes. 574 | for child in node.children { 575 | let mut prefix = prefix.clone(); 576 | prefix.append(&child.prefix); 577 | queue.push_back((prefix, child)); 578 | } 579 | } 580 | } 581 | } 582 | 583 | /// A wildcard node that was skipped during a tree search. 584 | /// 585 | /// Contains the state necessary to backtrack to the given node. 586 | struct Skipped<'node, 'path, T> { 587 | // The node that was skipped. 588 | node: &'node Node, 589 | 590 | /// The path at the time we skipped this node. 591 | path: &'path [u8], 592 | 593 | // The number of parameters that were present. 594 | params: usize, 595 | } 596 | 597 | impl Node { 598 | // Returns the node matching the given path. 599 | // 600 | // Returning an `UnsafeCell` allows us to avoid duplicating the logic between `Node::at` and 601 | // `Node::at_mut`, as Rust doesn't have a great way of abstracting over mutability. 602 | #[inline] 603 | pub fn at<'node, 'path>( 604 | &'node self, 605 | mut path: &'path [u8], 606 | ) -> Result<(&'node UnsafeCell, Params<'node, 'path>), MatchError> { 607 | let mut node = self; 608 | let mut backtracking = false; 609 | let mut params = Params::new(); 610 | let mut skipped: Vec> = Vec::new(); 611 | 612 | 'backtrack: loop { 613 | 'walk: loop { 614 | // Reached the end of the 615 | if path.len() <= node.prefix.len() { 616 | // Check for an exact match. 617 | if *path == *node.prefix { 618 | // Found the matching value. 619 | if let Some(ref value) = node.value { 620 | // Remap the keys of any route parameters we accumulated during the search. 621 | params.for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 622 | return Ok((value, params)); 623 | } 624 | } 625 | 626 | break 'walk; 627 | } 628 | 629 | // Otherwise, the path is longer than this node's prefix, search deeper. 630 | let (prefix, rest) = path.split_at(node.prefix.len()); 631 | 632 | // The prefix does not match. 633 | if *prefix != *node.prefix { 634 | break 'walk; 635 | } 636 | 637 | let previous = path; 638 | path = rest; 639 | 640 | // If we are currently backtracking, avoid searching static children 641 | // that we already searched. 642 | if !backtracking { 643 | let next = path[0]; 644 | 645 | // Find a child node that matches the next character in the path. 646 | if let Some(i) = node.indices.iter().position(|&c| c == next) { 647 | // Keep track of wildcard routes that we skip. 648 | // 649 | // We may end up needing to backtrack later in case we do not find a 650 | // match. 651 | if node.wild_child { 652 | skipped.push(Skipped { 653 | node, 654 | path: previous, 655 | params: params.len(), 656 | }); 657 | } 658 | 659 | // Continue searching. 660 | node = &node.children[i]; 661 | continue 'walk; 662 | } 663 | } 664 | 665 | // We didn't find a matching static child. 666 | // 667 | // If there are no wildcards, then there are no matching routes in the tree. 668 | if !node.wild_child { 669 | break 'walk; 670 | } 671 | 672 | // Continue searching in the wildcard child, which is kept at the end of the list. 673 | node = node.children.last().unwrap(); 674 | match node.node_type { 675 | NodeType::Param { suffix: false } => { 676 | // Check for more path segments. 677 | let terminator = match path.iter().position(|&c| c == b'/') { 678 | // Double `//` implying an empty parameter, no match. 679 | Some(0) => break 'walk, 680 | 681 | // Found another segment. 682 | Some(i) => i, 683 | 684 | // This is the last path segment. 685 | None => { 686 | // If this is the last path segment and there is a matching 687 | // value without a suffix, we have a match. 688 | let Some(ref value) = node.value else { 689 | break 'walk; 690 | }; 691 | 692 | // Store the parameter value. 693 | params.push(b"", path); 694 | 695 | // Remap the keys of any route parameters we accumulated during the search. 696 | params 697 | .for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 698 | 699 | return Ok((value, params)); 700 | } 701 | }; 702 | 703 | // Found another path segment. 704 | let (param, rest) = path.split_at(terminator); 705 | 706 | // If there is a static child, continue the search. 707 | let [child] = node.children.as_slice() else { 708 | break 'walk; 709 | }; 710 | 711 | // Store the parameter value. 712 | // Parameters are normalized so this key is irrelevant for now. 713 | params.push(b"", param); 714 | 715 | // Continue searching. 716 | path = rest; 717 | node = child; 718 | backtracking = false; 719 | continue 'walk; 720 | } 721 | 722 | NodeType::Param { suffix: true } => { 723 | // Check for more path segments. 724 | let slash = path.iter().position(|&c| c == b'/'); 725 | let terminator = match slash { 726 | // Double `//` implying an empty parameter, no match. 727 | Some(0) => break 'walk, 728 | 729 | // Found another segment. 730 | Some(i) => i + 1, 731 | 732 | // This is the last path segment. 733 | None => path.len(), 734 | }; 735 | 736 | for child in node.children.iter() { 737 | // Ensure there is a possible match with a non-zero suffix. 738 | if child.prefix.len() >= terminator { 739 | continue; 740 | } 741 | 742 | let suffix_start = terminator - child.prefix.len(); 743 | let (param, suffix) = path[..terminator].split_at(suffix_start); 744 | 745 | // Continue searching if the suffix matches. 746 | if *suffix == *child.prefix { 747 | node = child; 748 | path = &path[suffix_start..]; 749 | backtracking = false; 750 | // Parameters are normalized so this key is irrelevant for now. 751 | params.push(b"", param); 752 | continue 'walk; 753 | } 754 | } 755 | 756 | // If this is the last path segment and there is a matching 757 | // value without a suffix, we have a match. 758 | let value = match node.value { 759 | // Found the matching value. 760 | Some(ref value) if slash.is_none() => value, 761 | _ => break 'walk, 762 | }; 763 | 764 | // Store the parameter value. 765 | params.push(b"", path); 766 | 767 | // Remap the keys of any route parameters we accumulated during the search. 768 | params.for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 769 | 770 | return Ok((value, params)); 771 | } 772 | 773 | NodeType::CatchAll => { 774 | // Catch-all segments are only allowed at the end of the route, meaning 775 | // this node must contain the value. 776 | let value = match node.value { 777 | // Found the matching value. 778 | Some(ref value) => value, 779 | 780 | // Otherwise, there are no matching routes in the tree. 781 | None => return Err(MatchError::NotFound), 782 | }; 783 | 784 | // Remap the keys of any route parameters we accumulated during the search. 785 | params.for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 786 | 787 | // Store the final catch-all parameter (`{*...}`). 788 | let key = &node.prefix[2..node.prefix.len() - 1]; 789 | params.push(key, path); 790 | 791 | return Ok((value, params)); 792 | } 793 | 794 | _ => unreachable!(), 795 | } 796 | } 797 | 798 | // Try backtracking to any matching wildcard nodes that we skipped while 799 | // traversing the tree. 800 | while let Some(skipped) = skipped.pop() { 801 | if skipped.path.ends_with(path) { 802 | // Found a matching node, restore the search state. 803 | path = skipped.path; 804 | node = skipped.node; 805 | backtracking = true; 806 | params.truncate(skipped.params); 807 | continue 'backtrack; 808 | } 809 | } 810 | 811 | return Err(MatchError::NotFound); 812 | } 813 | } 814 | 815 | /// Test helper that ensures route priorities are consistent. 816 | #[cfg(feature = "__test_helpers")] 817 | pub fn check_priorities(&self) -> Result { 818 | let mut priority: u32 = 0; 819 | for child in &self.children { 820 | priority += child.check_priorities()?; 821 | } 822 | 823 | if self.value.is_some() { 824 | priority += 1; 825 | } 826 | 827 | if self.priority != priority { 828 | return Err((self.priority, priority)); 829 | } 830 | 831 | Ok(priority) 832 | } 833 | } 834 | 835 | /// An ordered list of route parameters keys for a specific route. 836 | /// 837 | /// To support conflicting routes like `/{a}/foo` and `/{b}/bar`, route parameters 838 | /// are normalized before being inserted into the tree. Parameter remapping are 839 | /// stored at nodes containing values, containing the "true" names of all route parameters 840 | /// for the given route. 841 | type ParamRemapping = Vec>; 842 | 843 | /// Returns `path` with normalized route parameters, and a parameter remapping 844 | /// to store at the node for this route. 845 | /// 846 | /// Note that the parameter remapping may contain unescaped characters. 847 | fn normalize_params( 848 | mut path: UnescapedRoute, 849 | ) -> Result<(UnescapedRoute, ParamRemapping), InsertError> { 850 | let mut start = 0; 851 | let mut original = ParamRemapping::new(); 852 | 853 | // Parameter names are normalized alphabetically. 854 | let mut next = b'a'; 855 | 856 | loop { 857 | // Find a wildcard to normalize. 858 | let mut wildcard = match find_wildcard(path.as_ref().slice_off(start))? { 859 | Some(wildcard) => wildcard, 860 | // No wildcard, we are done. 861 | None => return Ok((path, original)), 862 | }; 863 | 864 | wildcard.start += start; 865 | wildcard.end += start; 866 | 867 | // Ensure the parameter has a valid name. 868 | if wildcard.len() < 2 { 869 | return Err(InsertError::InvalidParam); 870 | } 871 | 872 | // We don't need to normalize catch-all parameters, as they are always 873 | // at the end of a route. 874 | if path[wildcard.clone()][1] == b'*' { 875 | start = wildcard.end; 876 | continue; 877 | } 878 | 879 | // Normalize the parameter. 880 | let removed = path.splice(wildcard.clone(), vec![b'{', next, b'}']); 881 | 882 | // Preserve the original name for remapping. 883 | let mut removed = removed.skip(1).collect::>(); 884 | removed.pop(); 885 | original.push(removed); 886 | 887 | next += 1; 888 | if next > b'z' { 889 | panic!("Too many route parameters."); 890 | } 891 | 892 | // Continue the search after the parameter we just normalized. 893 | start = wildcard.start + 3; 894 | } 895 | } 896 | 897 | /// Restores `route` to it's original, denormalized form. 898 | pub(crate) fn denormalize_params(route: &mut UnescapedRoute, params: &ParamRemapping) { 899 | let mut start = 0; 900 | let mut i = 0; 901 | 902 | loop { 903 | // Find a wildcard to denormalize. 904 | let mut wildcard = match find_wildcard(route.as_ref().slice_off(start)).unwrap() { 905 | Some(w) => w, 906 | None => return, 907 | }; 908 | 909 | wildcard.start += start; 910 | wildcard.end += start; 911 | 912 | // Get the corresponding parameter remapping. 913 | let mut next = match params.get(i) { 914 | Some(param) => param.clone(), 915 | None => return, 916 | }; 917 | 918 | // Denormalize this parameter. 919 | next.insert(0, b'{'); 920 | next.push(b'}'); 921 | let _ = route.splice(wildcard.clone(), next.clone()); 922 | 923 | i += 1; 924 | start = wildcard.start + next.len(); 925 | } 926 | } 927 | 928 | // Searches for a wildcard segment and checks the path for invalid characters. 929 | fn find_wildcard(path: UnescapedRef<'_>) -> Result>, InsertError> { 930 | for (start, &c) in path.iter().enumerate() { 931 | // Found an unescaped closing brace without a corresponding opening brace. 932 | if c == b'}' && !path.is_escaped(start) { 933 | return Err(InsertError::InvalidParam); 934 | } 935 | 936 | // Keep going until we find an unescaped opening brace. 937 | if c != b'{' || path.is_escaped(start) { 938 | continue; 939 | } 940 | 941 | // Ensure there is a non-empty parameter name. 942 | if path.get(start + 1) == Some(&b'}') { 943 | return Err(InsertError::InvalidParam); 944 | } 945 | 946 | // Find the corresponding closing brace. 947 | for (i, &c) in path.iter().enumerate().skip(start + 2) { 948 | match c { 949 | b'}' => { 950 | // This closing brace was escaped, keep searching. 951 | if path.is_escaped(i) { 952 | continue; 953 | } 954 | 955 | // Ensure catch-all parameters have a non-empty name. 956 | if path.get(i - 1) == Some(&b'*') { 957 | return Err(InsertError::InvalidParam); 958 | } 959 | 960 | return Ok(Some(start..i + 1)); 961 | } 962 | // `*` and `/` are invalid in parameter names. 963 | b'*' | b'/' => return Err(InsertError::InvalidParam), 964 | _ => {} 965 | } 966 | } 967 | 968 | // Missing closing brace. 969 | return Err(InsertError::InvalidParam); 970 | } 971 | 972 | Ok(None) 973 | } 974 | 975 | impl Clone for Node 976 | where 977 | T: Clone, 978 | { 979 | fn clone(&self) -> Node { 980 | let value = self.value.as_ref().map(|value| { 981 | // Safety: We only expose `&mut T` through `&mut self`. 982 | let value = unsafe { &*value.get() }; 983 | UnsafeCell::new(value.clone()) 984 | }); 985 | 986 | Node { 987 | value, 988 | prefix: self.prefix.clone(), 989 | wild_child: self.wild_child, 990 | node_type: self.node_type.clone(), 991 | indices: self.indices.clone(), 992 | children: self.children.clone(), 993 | remapping: self.remapping.clone(), 994 | priority: self.priority, 995 | } 996 | } 997 | } 998 | 999 | impl Default for Node { 1000 | fn default() -> Node { 1001 | Node { 1002 | remapping: ParamRemapping::new(), 1003 | prefix: UnescapedRoute::default(), 1004 | wild_child: false, 1005 | node_type: NodeType::Static, 1006 | indices: Vec::new(), 1007 | children: Vec::new(), 1008 | value: None, 1009 | priority: 0, 1010 | } 1011 | } 1012 | } 1013 | 1014 | impl fmt::Debug for Node 1015 | where 1016 | T: fmt::Debug, 1017 | { 1018 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1019 | // Safety: We only expose `&mut T` through `&mut self`. 1020 | let value = unsafe { self.value.as_ref().map(|x| &*x.get()) }; 1021 | 1022 | let mut f = f.debug_struct("Node"); 1023 | f.field("value", &value) 1024 | .field("prefix", &self.prefix) 1025 | .field("node_type", &self.node_type) 1026 | .field("children", &self.children); 1027 | 1028 | // Extra information for debugging purposes. 1029 | #[cfg(test)] 1030 | { 1031 | let indices = self 1032 | .indices 1033 | .iter() 1034 | .map(|&x| char::from_u32(x as _)) 1035 | .collect::>(); 1036 | 1037 | let params = self 1038 | .remapping 1039 | .iter() 1040 | .map(|x| std::str::from_utf8(x).unwrap()) 1041 | .collect::>(); 1042 | 1043 | f.field("indices", &indices).field("params", ¶ms); 1044 | } 1045 | 1046 | f.finish() 1047 | } 1048 | } 1049 | -------------------------------------------------------------------------------- /tests/insert.rs: -------------------------------------------------------------------------------- 1 | use matchit::{InsertError, Router}; 2 | 3 | struct InsertTest(Vec<(&'static str, Result<(), InsertError>)>); 4 | 5 | impl InsertTest { 6 | fn run(self) { 7 | let mut router = Router::new(); 8 | for (route, expected) in self.0 { 9 | let got = router.insert(route, route.to_owned()); 10 | assert_eq!(got, expected, "{route}"); 11 | } 12 | } 13 | } 14 | 15 | fn conflict(with: &'static str) -> InsertError { 16 | InsertError::Conflict { with: with.into() } 17 | } 18 | 19 | #[test] 20 | fn wildcard_conflict() { 21 | InsertTest(vec![ 22 | ("/cmd/{tool}/{sub}", Ok(())), 23 | ("/cmd/vet", Ok(())), 24 | ("/foo/bar", Ok(())), 25 | ("/foo/{name}", Ok(())), 26 | ("/foo/{names}", Err(conflict("/foo/{name}"))), 27 | ("/cmd/{*path}", Err(conflict("/cmd/{tool}/{sub}"))), 28 | ("/cmd/{xxx}/names", Ok(())), 29 | ("/cmd/{tool}/{xxx}/foo", Ok(())), 30 | ("/src/{*filepath}", Ok(())), 31 | ("/src/{file}", Err(conflict("/src/{*filepath}"))), 32 | ("/src/static.json", Ok(())), 33 | ("/src/$filepathx", Ok(())), 34 | ("/src/", Ok(())), 35 | ("/src/foo/bar", Ok(())), 36 | ("/src1/", Ok(())), 37 | ("/src1/{*filepath}", Ok(())), 38 | ("/src2{*filepath}", Ok(())), 39 | ("/src2/{*filepath}", Ok(())), 40 | ("/src2/", Ok(())), 41 | ("/src2", Ok(())), 42 | ("/src3", Ok(())), 43 | ("/src3/{*filepath}", Ok(())), 44 | ("/search/{query}", Ok(())), 45 | ("/search/valid", Ok(())), 46 | ("/user_{name}", Ok(())), 47 | ("/user_x", Ok(())), 48 | ("/user_{bar}", Err(conflict("/user_{name}"))), 49 | ("/id{id}", Ok(())), 50 | ("/id/{id}", Ok(())), 51 | ("/x/{id}", Ok(())), 52 | ("/x/{id}/", Ok(())), 53 | ("/x/{id}y", Ok(())), 54 | ("/x/{id}y/", Ok(())), 55 | ("/x/x{id}", Ok(())), 56 | ("/x/x{id}y", Ok(())), 57 | ("/qux/id", Ok(())), 58 | ("/qux/{id}y", Ok(())), 59 | ("/qux/{id}", Ok(())), 60 | ("/qux/{id}/", Ok(())), 61 | ("/qux/{id}x", Ok(())), 62 | ("/qux/x{id}y", Ok(())), 63 | ("/qux/x{id}", Ok(())), 64 | ("/qux/x{id}", Err(conflict("/qux/x{id}"))), 65 | ("/qux/x{id}y", Err(conflict("/qux/x{id}y"))), 66 | ("/bar/{id}", Ok(())), 67 | ("/bar/x{id}y", Ok(())), 68 | ]) 69 | .run() 70 | } 71 | 72 | #[test] 73 | fn invalid_catchall() { 74 | InsertTest(vec![ 75 | ("/non-leading-{*catchall}", Ok(())), 76 | ("/foo/bar{*catchall}", Ok(())), 77 | ("/src/{*filepath}x", Err(InsertError::InvalidCatchAll)), 78 | ("/src/{*filepath}/x", Err(InsertError::InvalidCatchAll)), 79 | ("/src2/", Ok(())), 80 | ("/src2/{*filepath}/x", Err(InsertError::InvalidCatchAll)), 81 | ]) 82 | .run() 83 | } 84 | 85 | #[test] 86 | fn catchall_root_conflict() { 87 | InsertTest(vec![("/", Ok(())), ("/{*filepath}", Ok(()))]).run() 88 | } 89 | 90 | #[test] 91 | fn child_conflict() { 92 | InsertTest(vec![ 93 | ("/cmd/vet", Ok(())), 94 | ("/cmd/{tool}", Ok(())), 95 | ("/cmd/{tool}/{sub}", Ok(())), 96 | ("/cmd/{tool}/misc", Ok(())), 97 | ("/cmd/{tool}/{bad}", Err(conflict("/cmd/{tool}/{sub}"))), 98 | ("/src/AUTHORS", Ok(())), 99 | ("/src/{*filepath}", Ok(())), 100 | ("/user_x", Ok(())), 101 | ("/user_{name}", Ok(())), 102 | ("/id/{id}", Ok(())), 103 | ("/id{id}", Ok(())), 104 | ("/{id}", Ok(())), 105 | ("/{*filepath}", Err(conflict("/{id}"))), 106 | ]) 107 | .run() 108 | } 109 | 110 | #[test] 111 | fn duplicates() { 112 | InsertTest(vec![ 113 | ("/", Ok(())), 114 | ("/", Err(conflict("/"))), 115 | ("/doc/", Ok(())), 116 | ("/doc/", Err(conflict("/doc/"))), 117 | ("/src/{*filepath}", Ok(())), 118 | ("/src/{*filepath}", Err(conflict("/src/{*filepath}"))), 119 | ("/search/{query}", Ok(())), 120 | ("/search/{query}", Err(conflict("/search/{query}"))), 121 | ("/user_{name}", Ok(())), 122 | ("/user_{name}", Err(conflict("/user_{name}"))), 123 | ]) 124 | .run() 125 | } 126 | 127 | #[test] 128 | fn unnamed_param() { 129 | InsertTest(vec![ 130 | ("/{}", Err(InsertError::InvalidParam)), 131 | ("/user{}/", Err(InsertError::InvalidParam)), 132 | ("/cmd/{}/", Err(InsertError::InvalidParam)), 133 | ("/src/{*}", Err(InsertError::InvalidParam)), 134 | ]) 135 | .run() 136 | } 137 | 138 | #[test] 139 | fn double_params() { 140 | InsertTest(vec![ 141 | ("/{foo}{bar}", Err(InsertError::InvalidParamSegment)), 142 | ("/{foo}{bar}/", Err(InsertError::InvalidParamSegment)), 143 | ("/{foo}{{*bar}/", Err(InsertError::InvalidParamSegment)), 144 | ]) 145 | .run() 146 | } 147 | 148 | #[test] 149 | fn normalized_conflict() { 150 | InsertTest(vec![ 151 | ("/x/{foo}/bar", Ok(())), 152 | ("/x/{bar}/bar", Err(conflict("/x/{foo}/bar"))), 153 | ("/{y}/bar/baz", Ok(())), 154 | ("/{y}/baz/baz", Ok(())), 155 | ("/{z}/bar/bat", Ok(())), 156 | ("/{z}/bar/baz", Err(conflict("/{y}/bar/baz"))), 157 | ]) 158 | .run() 159 | } 160 | 161 | #[test] 162 | fn more_conflicts() { 163 | InsertTest(vec![ 164 | ("/con{tact}", Ok(())), 165 | ("/who/are/{*you}", Ok(())), 166 | ("/who/foo/hello", Ok(())), 167 | ("/whose/{users}/{name}", Ok(())), 168 | ("/who/are/foo", Ok(())), 169 | ("/who/are/foo/bar", Ok(())), 170 | ("/con{nection}", Err(conflict("/con{tact}"))), 171 | ( 172 | "/whose/{users}/{user}", 173 | Err(conflict("/whose/{users}/{name}")), 174 | ), 175 | ]) 176 | .run() 177 | } 178 | 179 | #[test] 180 | fn catchall_static_overlap() { 181 | InsertTest(vec![ 182 | ("/bar", Ok(())), 183 | ("/bar/", Ok(())), 184 | ("/bar/{*foo}", Ok(())), 185 | ]) 186 | .run(); 187 | 188 | InsertTest(vec![ 189 | ("/foo", Ok(())), 190 | ("/{*bar}", Ok(())), 191 | ("/bar", Ok(())), 192 | ("/baz", Ok(())), 193 | ("/baz/{split}", Ok(())), 194 | ("/", Ok(())), 195 | ("/{*bar}", Err(conflict("/{*bar}"))), 196 | ("/{*zzz}", Err(conflict("/{*bar}"))), 197 | ("/{xxx}", Err(conflict("/{*bar}"))), 198 | ]) 199 | .run(); 200 | 201 | InsertTest(vec![ 202 | ("/{*bar}", Ok(())), 203 | ("/bar", Ok(())), 204 | ("/bar/x", Ok(())), 205 | ("/bar_{x}", Ok(())), 206 | ("/bar_{x}", Err(conflict("/bar_{x}"))), 207 | ("/bar_{x}/y", Ok(())), 208 | ("/bar/{x}", Ok(())), 209 | ]) 210 | .run(); 211 | } 212 | 213 | #[test] 214 | fn duplicate_conflict() { 215 | InsertTest(vec![ 216 | ("/hey", Ok(())), 217 | ("/hey/users", Ok(())), 218 | ("/hey/user", Ok(())), 219 | ("/hey/user", Err(conflict("/hey/user"))), 220 | ]) 221 | .run() 222 | } 223 | 224 | #[test] 225 | fn invalid_param() { 226 | InsertTest(vec![ 227 | ("{", Err(InsertError::InvalidParam)), 228 | ("}", Err(InsertError::InvalidParam)), 229 | ("x{y", Err(InsertError::InvalidParam)), 230 | ("x}", Err(InsertError::InvalidParam)), 231 | ]) 232 | .run(); 233 | } 234 | 235 | #[test] 236 | fn escaped_param() { 237 | InsertTest(vec![ 238 | ("{{", Ok(())), 239 | ("}}", Ok(())), 240 | ("xx}}", Ok(())), 241 | ("}}yy", Ok(())), 242 | ("}}yy{{}}", Ok(())), 243 | ("}}yy{{}}{{}}y{{", Ok(())), 244 | ("}}yy{{}}{{}}y{{", Err(conflict("}yy{}{}y{"))), 245 | ("/{{yy", Ok(())), 246 | ("/{yy}", Ok(())), 247 | ("/foo", Ok(())), 248 | ("/foo/{{", Ok(())), 249 | ("/foo/{{/{x}", Ok(())), 250 | ("/foo/{ba{{r}", Ok(())), 251 | ("/bar/{ba}}r}", Ok(())), 252 | ("/xxx/{x{{}}y}", Ok(())), 253 | ]) 254 | .run() 255 | } 256 | 257 | #[test] 258 | fn bare_catchall() { 259 | InsertTest(vec![("{*foo}", Ok(())), ("foo/{*bar}", Ok(()))]).run() 260 | } 261 | -------------------------------------------------------------------------------- /tests/match.rs: -------------------------------------------------------------------------------- 1 | use matchit::{MatchError, Router}; 2 | 3 | // https://github.com/ibraheemdev/matchit/issues/22 4 | #[test] 5 | fn partial_overlap() { 6 | let mut x = Router::new(); 7 | x.insert("/foo_bar", "Welcome!").unwrap(); 8 | x.insert("/foo/bar", "Welcome!").unwrap(); 9 | assert_eq!(x.at("/foo/").unwrap_err(), MatchError::NotFound); 10 | 11 | let mut x = Router::new(); 12 | x.insert("/foo", "Welcome!").unwrap(); 13 | x.insert("/foo/bar", "Welcome!").unwrap(); 14 | assert_eq!(x.at("/foo/").unwrap_err(), MatchError::NotFound); 15 | } 16 | 17 | // https://github.com/ibraheemdev/matchit/issues/31 18 | #[test] 19 | fn wildcard_overlap() { 20 | let mut router = Router::new(); 21 | router.insert("/path/foo", "foo").unwrap(); 22 | router.insert("/path/{*rest}", "wildcard").unwrap(); 23 | 24 | assert_eq!(router.at("/path/foo").map(|m| *m.value), Ok("foo")); 25 | assert_eq!(router.at("/path/bar").map(|m| *m.value), Ok("wildcard")); 26 | assert_eq!(router.at("/path/foo/").map(|m| *m.value), Ok("wildcard")); 27 | 28 | let mut router = Router::new(); 29 | router.insert("/path/foo/{arg}", "foo").unwrap(); 30 | router.insert("/path/{*rest}", "wildcard").unwrap(); 31 | 32 | assert_eq!(router.at("/path/foo/myarg").map(|m| *m.value), Ok("foo")); 33 | assert_eq!( 34 | router.at("/path/foo/myarg/").map(|m| *m.value), 35 | Ok("wildcard") 36 | ); 37 | assert_eq!( 38 | router.at("/path/foo/myarg/bar/baz").map(|m| *m.value), 39 | Ok("wildcard") 40 | ); 41 | } 42 | 43 | // https://github.com/ibraheemdev/matchit/issues/12 44 | #[test] 45 | fn overlapping_param_backtracking() { 46 | let mut matcher = Router::new(); 47 | 48 | matcher.insert("/{object}/{id}", "object with id").unwrap(); 49 | matcher 50 | .insert("/secret/{id}/path", "secret with id and path") 51 | .unwrap(); 52 | 53 | let matched = matcher.at("/secret/978/path").unwrap(); 54 | assert_eq!(matched.params.get("id"), Some("978")); 55 | 56 | let matched = matcher.at("/something/978").unwrap(); 57 | assert_eq!(matched.params.get("id"), Some("978")); 58 | assert_eq!(matched.params.get("object"), Some("something")); 59 | 60 | let matched = matcher.at("/secret/978").unwrap(); 61 | assert_eq!(matched.params.get("id"), Some("978")); 62 | } 63 | 64 | struct MatchTest { 65 | routes: Vec<&'static str>, 66 | matches: Vec<( 67 | &'static str, 68 | &'static str, 69 | Result, ()>, 70 | )>, 71 | } 72 | 73 | impl MatchTest { 74 | fn run(self) { 75 | let mut router = Router::new(); 76 | 77 | for route in self.routes { 78 | assert_eq!(router.insert(route, route.to_owned()), Ok(()), "{route}"); 79 | } 80 | 81 | router.check_priorities().unwrap(); 82 | 83 | for (path, route, params) in self.matches { 84 | match router.at(path) { 85 | Ok(x) => { 86 | assert_eq!(x.value, route); 87 | 88 | let got = x.params.iter().collect::>(); 89 | assert_eq!(params.unwrap(), got); 90 | 91 | router.at_mut(path).unwrap().value.push_str("Z"); 92 | assert!(router.at(path).unwrap().value.contains("Z")); 93 | router.at_mut(path).unwrap().value.pop(); 94 | } 95 | Err(err) => { 96 | if let Ok(params) = params { 97 | panic!("{err} for {path} ({params:?})"); 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | macro_rules! p { 106 | ($($k:expr => $v:expr),* $(,)?) => { 107 | Ok(vec![$(($k, $v)),*]) 108 | }; 109 | } 110 | 111 | // https://github.com/ibraheemdev/matchit/issues/42 112 | #[test] 113 | fn bare_catchall() { 114 | MatchTest { 115 | routes: vec!["{*foo}", "foo/{*bar}"], 116 | matches: vec![ 117 | ("x/y", "{*foo}", p! { "foo" => "x/y" }), 118 | ("/x/y", "{*foo}", p! { "foo" => "/x/y" }), 119 | ("/foo/x/y", "{*foo}", p! { "foo" => "/foo/x/y" }), 120 | ("foo/x/y", "foo/{*bar}", p! { "bar" => "x/y" }), 121 | ], 122 | } 123 | .run() 124 | } 125 | 126 | #[test] 127 | fn normalized() { 128 | MatchTest { 129 | routes: vec![ 130 | "/x/{foo}/bar", 131 | "/x/{bar}/baz", 132 | "/{foo}/{baz}/bax", 133 | "/{foo}/{bar}/baz", 134 | "/{fod}/{baz}/{bax}/foo", 135 | "/{fod}/baz/bax/foo", 136 | "/{foo}/baz/bax", 137 | "/{bar}/{bay}/bay", 138 | "/s", 139 | "/s/s", 140 | "/s/s/s", 141 | "/s/s/s/s", 142 | "/s/s/{s}/x", 143 | "/s/s/{y}/d", 144 | ], 145 | matches: vec![ 146 | ("/x/foo/bar", "/x/{foo}/bar", p! { "foo" => "foo" }), 147 | ("/x/foo/baz", "/x/{bar}/baz", p! { "bar" => "foo" }), 148 | ( 149 | "/y/foo/baz", 150 | "/{foo}/{bar}/baz", 151 | p! { "foo" => "y", "bar" => "foo" }, 152 | ), 153 | ( 154 | "/y/foo/bax", 155 | "/{foo}/{baz}/bax", 156 | p! { "foo" => "y", "baz" => "foo" }, 157 | ), 158 | ( 159 | "/y/baz/baz", 160 | "/{foo}/{bar}/baz", 161 | p! { "foo" => "y", "bar" => "baz" }, 162 | ), 163 | ("/y/baz/bax/foo", "/{fod}/baz/bax/foo", p! { "fod" => "y" }), 164 | ( 165 | "/y/baz/b/foo", 166 | "/{fod}/{baz}/{bax}/foo", 167 | p! { "fod" => "y", "baz" => "baz", "bax" => "b" }, 168 | ), 169 | ("/y/baz/bax", "/{foo}/baz/bax", p! { "foo" => "y" }), 170 | ( 171 | "/z/bar/bay", 172 | "/{bar}/{bay}/bay", 173 | p! { "bar" => "z", "bay" => "bar" }, 174 | ), 175 | ("/s", "/s", p! {}), 176 | ("/s/s", "/s/s", p! {}), 177 | ("/s/s/s", "/s/s/s", p! {}), 178 | ("/s/s/s/s", "/s/s/s/s", p! {}), 179 | ("/s/s/s/x", "/s/s/{s}/x", p! { "s" => "s" }), 180 | ("/s/s/s/d", "/s/s/{y}/d", p! { "y" => "s" }), 181 | ], 182 | } 183 | .run() 184 | } 185 | 186 | #[test] 187 | fn blog() { 188 | MatchTest { 189 | routes: vec![ 190 | "/{page}", 191 | "/posts/{year}/{month}/{post}", 192 | "/posts/{year}/{month}/index", 193 | "/posts/{year}/top", 194 | "/static/{*path}", 195 | "/favicon.ico", 196 | ], 197 | matches: vec![ 198 | ("/about", "/{page}", p! { "page" => "about" }), 199 | ( 200 | "/posts/2021/01/rust", 201 | "/posts/{year}/{month}/{post}", 202 | p! { "year" => "2021", "month" => "01", "post" => "rust" }, 203 | ), 204 | ( 205 | "/posts/2021/01/index", 206 | "/posts/{year}/{month}/index", 207 | p! { "year" => "2021", "month" => "01" }, 208 | ), 209 | ( 210 | "/posts/2021/top", 211 | "/posts/{year}/top", 212 | p! { "year" => "2021" }, 213 | ), 214 | ( 215 | "/static/foo.png", 216 | "/static/{*path}", 217 | p! { "path" => "foo.png" }, 218 | ), 219 | ("/favicon.ico", "/favicon.ico", p! {}), 220 | ], 221 | } 222 | .run() 223 | } 224 | 225 | #[test] 226 | fn double_overlap() { 227 | MatchTest { 228 | routes: vec![ 229 | "/{object}/{id}", 230 | "/secret/{id}/path", 231 | "/secret/978", 232 | "/other/{object}/{id}/", 233 | "/other/an_object/{id}", 234 | "/other/static/path", 235 | "/other/long/static/path/", 236 | ], 237 | matches: vec![ 238 | ( 239 | "/secret/978/path", 240 | "/secret/{id}/path", 241 | p! { "id" => "978" }, 242 | ), 243 | ( 244 | "/some_object/978", 245 | "/{object}/{id}", 246 | p! { "object" => "some_object", "id" => "978" }, 247 | ), 248 | ("/secret/978", "/secret/978", p! {}), 249 | ("/super_secret/978/", "/{object}/{id}", Err(())), 250 | ( 251 | "/other/object/1/", 252 | "/other/{object}/{id}/", 253 | p! { "object" => "object", "id" => "1" }, 254 | ), 255 | ("/other/object/1/2", "/other/{object}/{id}", Err(())), 256 | ( 257 | "/other/an_object/1", 258 | "/other/an_object/{id}", 259 | p! { "id" => "1" }, 260 | ), 261 | ("/other/static/path", "/other/static/path", p! {}), 262 | ( 263 | "/other/long/static/path/", 264 | "/other/long/static/path/", 265 | p! {}, 266 | ), 267 | ], 268 | } 269 | .run() 270 | } 271 | 272 | #[test] 273 | fn catchall_off_by_one() { 274 | MatchTest { 275 | routes: vec!["/foo/{*catchall}", "/bar", "/bar/", "/bar/{*catchall}"], 276 | matches: vec![ 277 | ("/foo", "", Err(())), 278 | ("/foo/", "", Err(())), 279 | ("/foo/x", "/foo/{*catchall}", p! { "catchall" => "x" }), 280 | ("/bar", "/bar", p! {}), 281 | ("/bar/", "/bar/", p! {}), 282 | ("/bar/x", "/bar/{*catchall}", p! { "catchall" => "x" }), 283 | ], 284 | } 285 | .run() 286 | } 287 | 288 | #[test] 289 | fn overlap() { 290 | MatchTest { 291 | routes: vec![ 292 | "/foo", 293 | "/bar", 294 | "/{*bar}", 295 | "/baz", 296 | "/baz/", 297 | "/baz/x", 298 | "/baz/{xxx}", 299 | "/", 300 | "/xxx/{*x}", 301 | "/xxx/", 302 | ], 303 | matches: vec![ 304 | ("/foo", "/foo", p! {}), 305 | ("/bar", "/bar", p! {}), 306 | ("/baz", "/baz", p! {}), 307 | ("/baz/", "/baz/", p! {}), 308 | ("/baz/x", "/baz/x", p! {}), 309 | ("/???", "/{*bar}", p! { "bar" => "???" }), 310 | ("/", "/", p! {}), 311 | ("", "", Err(())), 312 | ("/xxx/y", "/xxx/{*x}", p! { "x" => "y" }), 313 | ("/xxx/", "/xxx/", p! {}), 314 | ("/xxx", "/{*bar}", p! { "bar" => "xxx" }), 315 | ], 316 | } 317 | .run() 318 | } 319 | 320 | #[test] 321 | fn missing_trailing_slash_param() { 322 | MatchTest { 323 | routes: vec!["/foo/{object}/{id}", "/foo/bar/baz", "/foo/secret/978/"], 324 | matches: vec![ 325 | ("/foo/secret/978/", "/foo/secret/978/", p! {}), 326 | ( 327 | "/foo/secret/978", 328 | "/foo/{object}/{id}", 329 | p! { "object" => "secret", "id" => "978" }, 330 | ), 331 | ], 332 | } 333 | .run() 334 | } 335 | 336 | #[test] 337 | fn extra_trailing_slash_param() { 338 | MatchTest { 339 | routes: vec!["/foo/{object}/{id}", "/foo/bar/baz", "/foo/secret/978"], 340 | matches: vec![ 341 | ("/foo/secret/978/", "", Err(())), 342 | ("/foo/secret/978", "/foo/secret/978", p! {}), 343 | ], 344 | } 345 | .run() 346 | } 347 | 348 | #[test] 349 | fn missing_trailing_slash_catch_all() { 350 | MatchTest { 351 | routes: vec!["/foo/{*bar}", "/foo/bar/baz", "/foo/secret/978/"], 352 | matches: vec![ 353 | ( 354 | "/foo/secret/978", 355 | "/foo/{*bar}", 356 | p! { "bar" => "secret/978" }, 357 | ), 358 | ("/foo/secret/978/", "/foo/secret/978/", p! {}), 359 | ], 360 | } 361 | .run() 362 | } 363 | 364 | #[test] 365 | fn extra_trailing_slash_catch_all() { 366 | MatchTest { 367 | routes: vec!["/foo/{*bar}", "/foo/bar/baz", "/foo/secret/978"], 368 | matches: vec![ 369 | ( 370 | "/foo/secret/978/", 371 | "/foo/{*bar}", 372 | p! { "bar" => "secret/978/" }, 373 | ), 374 | ("/foo/secret/978", "/foo/secret/978", p! {}), 375 | ], 376 | } 377 | .run() 378 | } 379 | 380 | #[test] 381 | fn double_overlap_trailing_slash() { 382 | MatchTest { 383 | routes: vec![ 384 | "/{object}/{id}", 385 | "/secret/{id}/path", 386 | "/secret/978/", 387 | "/other/{object}/{id}/", 388 | "/other/an_object/{id}", 389 | "/other/static/path", 390 | "/other/long/static/path/", 391 | ], 392 | matches: vec![ 393 | ("/secret/978/path/", "", Err(())), 394 | ("/object/id/", "", Err(())), 395 | ("/object/id/path", "", Err(())), 396 | ("/other/object/1", "", Err(())), 397 | ("/other/object/1/2", "", Err(())), 398 | ( 399 | "/other/an_object/1/", 400 | "/other/{object}/{id}/", 401 | p! { "object" => "an_object", "id" => "1" }, 402 | ), 403 | ( 404 | "/other/static/path/", 405 | "/other/{object}/{id}/", 406 | p! { "object" => "static", "id" => "path" }, 407 | ), 408 | ("/other/long/static/path", "", Err(())), 409 | ("/other/object/static/path", "", Err(())), 410 | ], 411 | } 412 | .run() 413 | } 414 | 415 | #[test] 416 | fn trailing_slash_overlap() { 417 | MatchTest { 418 | routes: vec!["/foo/{x}/baz/", "/foo/{x}/baz", "/foo/bar/bar"], 419 | matches: vec![ 420 | ("/foo/x/baz/", "/foo/{x}/baz/", p! { "x" => "x" }), 421 | ("/foo/x/baz", "/foo/{x}/baz", p! { "x" => "x" }), 422 | ("/foo/bar/bar", "/foo/bar/bar", p! {}), 423 | ], 424 | } 425 | .run() 426 | } 427 | 428 | #[test] 429 | fn trailing_slash() { 430 | MatchTest { 431 | routes: vec![ 432 | "/hi", 433 | "/b/", 434 | "/search/{query}", 435 | "/cmd/{tool}/", 436 | "/src/{*filepath}", 437 | "/x", 438 | "/x/y", 439 | "/y/", 440 | "/y/z", 441 | "/0/{id}", 442 | "/0/{id}/1", 443 | "/1/{id}/", 444 | "/1/{id}/2", 445 | "/aa", 446 | "/a/", 447 | "/admin", 448 | "/admin/static", 449 | "/admin/{category}", 450 | "/admin/{category}/{page}", 451 | "/doc", 452 | "/doc/rust_faq.html", 453 | "/doc/rust1.26.html", 454 | "/no/a", 455 | "/no/b", 456 | "/no/a/b/{*other}", 457 | "/api/{page}/{name}", 458 | "/api/hello/{name}/bar/", 459 | "/api/bar/{name}", 460 | "/api/baz/foo", 461 | "/api/baz/foo/bar", 462 | "/foo/{p}", 463 | ], 464 | matches: vec![ 465 | ("/hi/", "", Err(())), 466 | ("/b", "", Err(())), 467 | ("/search/rustacean/", "", Err(())), 468 | ("/cmd/vet", "", Err(())), 469 | ("/src", "", Err(())), 470 | ("/src/", "", Err(())), 471 | ("/x/", "", Err(())), 472 | ("/y", "", Err(())), 473 | ("/0/rust/", "", Err(())), 474 | ("/1/rust", "", Err(())), 475 | ("/a", "", Err(())), 476 | ("/admin/", "", Err(())), 477 | ("/doc/", "", Err(())), 478 | ("/admin/static/", "", Err(())), 479 | ("/admin/cfg/", "", Err(())), 480 | ("/admin/cfg/users/", "", Err(())), 481 | ("/api/hello/x/bar", "", Err(())), 482 | ("/api/baz/foo/", "", Err(())), 483 | ("/api/baz/bax/", "", Err(())), 484 | ("/api/bar/huh/", "", Err(())), 485 | ("/api/baz/foo/bar/", "", Err(())), 486 | ("/api/world/abc/", "", Err(())), 487 | ("/foo/pp/", "", Err(())), 488 | ("/", "", Err(())), 489 | ("/no", "", Err(())), 490 | ("/no/", "", Err(())), 491 | ("/no/a/b", "", Err(())), 492 | ("/no/a/b/", "", Err(())), 493 | ("/_", "", Err(())), 494 | ("/_/", "", Err(())), 495 | ("/api", "", Err(())), 496 | ("/api/", "", Err(())), 497 | ("/api/hello/x/foo", "", Err(())), 498 | ("/api/baz/foo/bad", "", Err(())), 499 | ("/foo/p/p", "", Err(())), 500 | ], 501 | } 502 | .run() 503 | } 504 | 505 | #[test] 506 | fn backtracking_trailing_slash() { 507 | MatchTest { 508 | routes: vec!["/a/{b}/{c}", "/a/b/{c}/d/"], 509 | matches: vec![("/a/b/c/d", "", Err(()))], 510 | } 511 | .run() 512 | } 513 | 514 | #[test] 515 | fn root_trailing_slash() { 516 | MatchTest { 517 | routes: vec!["/foo", "/bar", "/{baz}"], 518 | matches: vec![("/", "", Err(()))], 519 | } 520 | .run() 521 | } 522 | 523 | #[test] 524 | fn catchall_overlap() { 525 | MatchTest { 526 | routes: vec!["/yyy/{*x}", "/yyy{*x}"], 527 | matches: vec![ 528 | ("/yyy/y", "/yyy/{*x}", p! { "x" => "y" }), 529 | ("/yyy/", "/yyy{*x}", p! { "x" => "/" }), 530 | ], 531 | } 532 | .run(); 533 | } 534 | 535 | #[test] 536 | fn escaped() { 537 | MatchTest { 538 | routes: vec![ 539 | "/", 540 | "/{{", 541 | "/}}", 542 | "/{{x", 543 | "/}}y{{", 544 | "/xy{{", 545 | "/{{/xyz", 546 | "/{ba{{r}", 547 | "/{ba{{r}/", 548 | "/{ba{{r}/x", 549 | "/baz/{xxx}", 550 | "/baz/{xxx}/xy{{", 551 | "/baz/{xxx}/}}xy{{{{", 552 | "/{{/{x}", 553 | "/xxx/", 554 | "/xxx/{x}}{{}}}}{{}}{{{{}}y}", 555 | ], 556 | matches: vec![ 557 | ("/", "/", p! {}), 558 | ("/{", "/{{", p! {}), 559 | ("/}", "/}}", p! {}), 560 | ("/{x", "/{{x", p! {}), 561 | ("/}y{", "/}}y{{", p! {}), 562 | ("/xy{", "/xy{{", p! {}), 563 | ("/{/xyz", "/{{/xyz", p! {}), 564 | ("/foo", "/{ba{{r}", p! { "ba{r" => "foo" }), 565 | ("/{{", "/{ba{{r}", p! { "ba{r" => "{{" }), 566 | ("/{{}}/", "/{ba{{r}/", p! { "ba{r" => "{{}}" }), 567 | ("/{{}}{{/x", "/{ba{{r}/x", p! { "ba{r" => "{{}}{{" }), 568 | ("/baz/x", "/baz/{xxx}", p! { "xxx" => "x" }), 569 | ("/baz/x/xy{", "/baz/{xxx}/xy{{", p! { "xxx" => "x" }), 570 | ("/baz/x/xy{{", "", Err(())), 571 | ("/baz/x/}xy{{", "/baz/{xxx}/}}xy{{{{", p! { "xxx" => "x" }), 572 | ("/{/{{", "/{{/{x}", p! { "x" => "{{" }), 573 | ("/xxx", "/{ba{{r}", p! { "ba{r" => "xxx" }), 574 | ("/xxx/", "/xxx/", p!()), 575 | ( 576 | "/xxx/foo", 577 | "/xxx/{x}}{{}}}}{{}}{{{{}}y}", 578 | p! { "x}{}}{}{{}y" => "foo" }, 579 | ), 580 | ], 581 | } 582 | .run() 583 | } 584 | 585 | #[test] 586 | fn empty_param() { 587 | MatchTest { 588 | routes: vec![ 589 | "/y/{foo}", 590 | "/x/{foo}/z", 591 | "/z/{*foo}", 592 | "/a/x{foo}", 593 | "/b/{foo}x", 594 | ], 595 | matches: vec![ 596 | ("/y/", "", Err(())), 597 | ("/x//z", "", Err(())), 598 | ("/z/", "", Err(())), 599 | ("/a/x", "", Err(())), 600 | ("/b/x", "", Err(())), 601 | ], 602 | } 603 | .run(); 604 | } 605 | 606 | #[test] 607 | fn wildcard_suffix() { 608 | MatchTest { 609 | routes: vec![ 610 | "/", 611 | "/{foo}x", 612 | "/foox", 613 | "/{foo}x/bar", 614 | "/{foo}x/bar/baz", 615 | "/x{foo}", 616 | "/x{foo}/bar", 617 | ], 618 | matches: vec![ 619 | ("/", "/", p! {}), 620 | ("/foox", "/foox", p! {}), 621 | ("/barx", "/{foo}x", p! { "foo" => "bar" }), 622 | ("/mx", "/{foo}x", p! { "foo" => "m" }), 623 | ("/mx/", "", Err(())), 624 | ("/mxm", "", Err(())), 625 | ("/mx/bar", "/{foo}x/bar", p! { "foo" => "m" }), 626 | ("/mxm/bar", "", Err(())), 627 | ("/x", "", Err(())), 628 | ("/xfoo", "/x{foo}", p! { "foo" => "foo" }), 629 | ("/xfoox", "/x{foo}", p! { "foo" => "foox" }), 630 | ("/xfoox/bar", "/x{foo}/bar", p! { "foo" => "foox" }), 631 | ("/xfoox/bar/baz", "/{foo}x/bar/baz", p! { "foo" => "xfoo" }), 632 | ], 633 | } 634 | .run(); 635 | } 636 | 637 | #[test] 638 | fn mixed_wildcard_suffix() { 639 | MatchTest { 640 | routes: vec![ 641 | "/", 642 | "/{f}o/b", 643 | "/{f}oo/b", 644 | "/{f}ooo/b", 645 | "/{f}oooo/b", 646 | "/foo/b", 647 | "/foo/{b}", 648 | "/foo/{b}one", 649 | "/foo/{b}one/", 650 | "/foo/{b}two", 651 | "/foo/{b}/one", 652 | "/foo/{b}one/one", 653 | "/foo/{b}two/one", 654 | "/foo/{b}one/one/", 655 | "/bar/{b}one", 656 | "/bar/{b}", 657 | "/bar/{b}/baz", 658 | "/bar/{b}one/baz", 659 | "/baz/{b}/bar", 660 | "/baz/{b}one/bar", 661 | ], 662 | matches: vec![ 663 | ("/", "/", p! {}), 664 | ("/o/b", "", Err(())), 665 | ("/fo/b", "/{f}o/b", p! { "f" => "f" }), 666 | ("/foo/b", "/foo/b", p! {}), 667 | ("/fooo/b", "/{f}ooo/b", p! { "f" => "f" }), 668 | ("/foooo/b", "/{f}oooo/b", p! { "f" => "f" }), 669 | ("/foo/b/", "", Err(())), 670 | ("/foooo/b/", "", Err(())), 671 | ("/foo/bb", "/foo/{b}", p! { "b" => "bb" }), 672 | ("/foo/bone", "/foo/{b}one", p! { "b" => "b" }), 673 | ("/foo/bone/", "/foo/{b}one/", p! { "b" => "b" }), 674 | ("/foo/btwo", "/foo/{b}two", p! { "b" => "b" }), 675 | ("/foo/btwo/", "", Err(())), 676 | ("/foo/b/one", "/foo/{b}/one", p! { "b" => "b" }), 677 | ("/foo/bone/one", "/foo/{b}one/one", p! { "b" => "b" }), 678 | ("/foo/bone/one/", "/foo/{b}one/one/", p! { "b" => "b" }), 679 | ("/foo/btwo/one", "/foo/{b}two/one", p! { "b" => "b" }), 680 | ("/bar/b", "/bar/{b}", p! { "b" => "b" }), 681 | ("/bar/b/baz", "/bar/{b}/baz", p! { "b" => "b" }), 682 | ("/bar/bone", "/bar/{b}one", p! { "b" => "b" }), 683 | ("/bar/bone/baz", "/bar/{b}one/baz", p! { "b" => "b" }), 684 | ("/baz/b/bar", "/baz/{b}/bar", p! { "b" => "b" }), 685 | ("/baz/bone/bar", "/baz/{b}one/bar", p! { "b" => "b" }), 686 | ], 687 | } 688 | .run(); 689 | } 690 | 691 | #[test] 692 | fn basic() { 693 | MatchTest { 694 | routes: vec![ 695 | "/hi", 696 | "/contact", 697 | "/co", 698 | "/c", 699 | "/a", 700 | "/ab", 701 | "/doc/", 702 | "/doc/rust_faq.html", 703 | "/doc/rust1.26.html", 704 | "/ʯ", 705 | "/β", 706 | "/sd!here", 707 | "/sd$here", 708 | "/sd&here", 709 | "/sd'here", 710 | "/sd(here", 711 | "/sd)here", 712 | "/sd+here", 713 | "/sd,here", 714 | "/sd;here", 715 | "/sd=here", 716 | ], 717 | matches: vec![ 718 | ("/a", "/a", p! {}), 719 | ("", "/", Err(())), 720 | ("/hi", "/hi", p! {}), 721 | ("/contact", "/contact", p! {}), 722 | ("/co", "/co", p! {}), 723 | ("", "/con", Err(())), 724 | ("", "/cona", Err(())), 725 | ("", "/no", Err(())), 726 | ("/ab", "/ab", p! {}), 727 | ("/ʯ", "/ʯ", p! {}), 728 | ("/β", "/β", p! {}), 729 | ("/sd!here", "/sd!here", p! {}), 730 | ("/sd$here", "/sd$here", p! {}), 731 | ("/sd&here", "/sd&here", p! {}), 732 | ("/sd'here", "/sd'here", p! {}), 733 | ("/sd(here", "/sd(here", p! {}), 734 | ("/sd)here", "/sd)here", p! {}), 735 | ("/sd+here", "/sd+here", p! {}), 736 | ("/sd,here", "/sd,here", p! {}), 737 | ("/sd;here", "/sd;here", p! {}), 738 | ("/sd=here", "/sd=here", p! {}), 739 | ], 740 | } 741 | .run() 742 | } 743 | 744 | #[test] 745 | fn wildcard() { 746 | MatchTest { 747 | routes: vec![ 748 | "/", 749 | "/cmd/{tool}/", 750 | "/cmd/{tool2}/{sub}", 751 | "/cmd/whoami", 752 | "/cmd/whoami/root", 753 | "/cmd/whoami/root/", 754 | "/src", 755 | "/src/", 756 | "/src/{*filepath}", 757 | "/search/", 758 | "/search/{query}", 759 | "/search/actix-web", 760 | "/search/google", 761 | "/user_{name}", 762 | "/user_{name}/about", 763 | "/files/{dir}/{*filepath}", 764 | "/doc/", 765 | "/doc/rust_faq.html", 766 | "/doc/rust1.26.html", 767 | "/info/{user}/public", 768 | "/info/{user}/project/{project}", 769 | "/info/{user}/project/rustlang", 770 | "/aa/{*xx}", 771 | "/ab/{*xx}", 772 | "/ab/hello{*xx}", 773 | "/{cc}", 774 | "/c1/{dd}/e", 775 | "/c1/{dd}/e1", 776 | "/{cc}/cc", 777 | "/{cc}/{dd}/ee", 778 | "/{cc}/{dd}/{ee}/ff", 779 | "/{cc}/{dd}/{ee}/{ff}/gg", 780 | "/{cc}/{dd}/{ee}/{ff}/{gg}/hh", 781 | "/get/test/abc/", 782 | "/get/{param}/abc/", 783 | "/something/{paramname}/thirdthing", 784 | "/something/secondthing/test", 785 | "/get/abc", 786 | "/get/{param}", 787 | "/get/abc/123abc", 788 | "/get/abc/{param}", 789 | "/get/abc/123abc/xxx8", 790 | "/get/abc/123abc/{param}", 791 | "/get/abc/123abc/xxx8/1234", 792 | "/get/abc/123abc/xxx8/{param}", 793 | "/get/abc/123abc/xxx8/1234/ffas", 794 | "/get/abc/123abc/xxx8/1234/{param}", 795 | "/get/abc/123abc/xxx8/1234/kkdd/12c", 796 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 797 | "/get/abc/{param}/test", 798 | "/get/abc/123abd/{param}", 799 | "/get/abc/123abddd/{param}", 800 | "/get/abc/123/{param}", 801 | "/get/abc/123abg/{param}", 802 | "/get/abc/123abf/{param}", 803 | "/get/abc/123abfff/{param}", 804 | ], 805 | matches: vec![ 806 | ("/", "/", p! {}), 807 | ("/cmd/test", "/cmd/{tool}/", Err(())), 808 | ("/cmd/test/", "/cmd/{tool}/", p! { "tool" => "test" }), 809 | ( 810 | "/cmd/test/3", 811 | "/cmd/{tool2}/{sub}", 812 | p! { "tool2" => "test", "sub" => "3" }, 813 | ), 814 | ("/cmd/who", "/cmd/{tool}/", Err(())), 815 | ("/cmd/who/", "/cmd/{tool}/", p! { "tool" => "who" }), 816 | ("/cmd/whoami", "/cmd/whoami", p! {}), 817 | ("/cmd/whoami/", "/cmd/{tool}/", p! { "tool" => "whoami" }), 818 | ( 819 | "/cmd/whoami/r", 820 | "/cmd/{tool2}/{sub}", 821 | p! { "tool2" => "whoami", "sub" => "r" }, 822 | ), 823 | ("/cmd/whoami/r/", "/cmd/{tool}/{sub}", Err(())), 824 | ("/cmd/whoami/root", "/cmd/whoami/root", p! {}), 825 | ("/cmd/whoami/root/", "/cmd/whoami/root/", p! {}), 826 | ("/src", "/src", p! {}), 827 | ("/src/", "/src/", p! {}), 828 | ( 829 | "/src/some/file.png", 830 | "/src/{*filepath}", 831 | p! { "filepath" => "some/file.png" }, 832 | ), 833 | ("/search/", "/search/", p! {}), 834 | ( 835 | "/search/actix", 836 | "/search/{query}", 837 | p! { "query" => "actix" }, 838 | ), 839 | ("/search/actix-web", "/search/actix-web", p! {}), 840 | ( 841 | "/search/someth!ng+in+ünìcodé", 842 | "/search/{query}", 843 | p! { "query" => "someth!ng+in+ünìcodé" }, 844 | ), 845 | ("/search/someth!ng+in+ünìcodé/", "", Err(())), 846 | ( 847 | "/user_rustacean", 848 | "/user_{name}", 849 | p! { "name" => "rustacean" }, 850 | ), 851 | ( 852 | "/user_rustacean/about", 853 | "/user_{name}/about", 854 | p! { "name" => "rustacean" }, 855 | ), 856 | ( 857 | "/files/js/inc/framework.js", 858 | "/files/{dir}/{*filepath}", 859 | p! { "dir" => "js", "filepath" => "inc/framework.js" }, 860 | ), 861 | ( 862 | "/info/gordon/public", 863 | "/info/{user}/public", 864 | p! { "user" => "gordon" }, 865 | ), 866 | ( 867 | "/info/gordon/project/rust", 868 | "/info/{user}/project/{project}", 869 | p! { "user" => "gordon", "project" => "rust" }, 870 | ), 871 | ( 872 | "/info/gordon/project/rustlang", 873 | "/info/{user}/project/rustlang", 874 | p! { "user" => "gordon" }, 875 | ), 876 | ("/aa/", "/", Err(())), 877 | ("/aa/aa", "/aa/{*xx}", p! { "xx" => "aa" }), 878 | ("/ab/ab", "/ab/{*xx}", p! { "xx" => "ab" }), 879 | ("/ab/hello-world", "/ab/hello{*xx}", p! { "xx" => "-world" }), 880 | ("/a", "/{cc}", p! { "cc" => "a" }), 881 | ("/all", "/{cc}", p! { "cc" => "all" }), 882 | ("/d", "/{cc}", p! { "cc" => "d" }), 883 | ("/ad", "/{cc}", p! { "cc" => "ad" }), 884 | ("/dd", "/{cc}", p! { "cc" => "dd" }), 885 | ("/dddaa", "/{cc}", p! { "cc" => "dddaa" }), 886 | ("/aa", "/{cc}", p! { "cc" => "aa" }), 887 | ("/aaa", "/{cc}", p! { "cc" => "aaa" }), 888 | ("/aaa/cc", "/{cc}/cc", p! { "cc" => "aaa" }), 889 | ("/ab", "/{cc}", p! { "cc" => "ab" }), 890 | ("/abb", "/{cc}", p! { "cc" => "abb" }), 891 | ("/abb/cc", "/{cc}/cc", p! { "cc" => "abb" }), 892 | ("/allxxxx", "/{cc}", p! { "cc" => "allxxxx" }), 893 | ("/alldd", "/{cc}", p! { "cc" => "alldd" }), 894 | ("/all/cc", "/{cc}/cc", p! { "cc" => "all" }), 895 | ("/a/cc", "/{cc}/cc", p! { "cc" => "a" }), 896 | ("/c1/d/e", "/c1/{dd}/e", p! { "dd" => "d" }), 897 | ("/c1/d/e1", "/c1/{dd}/e1", p! { "dd" => "d" }), 898 | ( 899 | "/c1/d/ee", 900 | "/{cc}/{dd}/ee", 901 | p! { "cc" => "c1", "dd" => "d" }, 902 | ), 903 | ("/cc/cc", "/{cc}/cc", p! { "cc" => "cc" }), 904 | ("/ccc/cc", "/{cc}/cc", p! { "cc" => "ccc" }), 905 | ("/deedwjfs/cc", "/{cc}/cc", p! { "cc" => "deedwjfs" }), 906 | ("/acllcc/cc", "/{cc}/cc", p! { "cc" => "acllcc" }), 907 | ("/get/test/abc/", "/get/test/abc/", p! {}), 908 | ("/get/te/abc/", "/get/{param}/abc/", p! { "param" => "te" }), 909 | ( 910 | "/get/testaa/abc/", 911 | "/get/{param}/abc/", 912 | p! { "param" => "testaa" }, 913 | ), 914 | ("/get/xx/abc/", "/get/{param}/abc/", p! { "param" => "xx" }), 915 | ("/get/tt/abc/", "/get/{param}/abc/", p! { "param" => "tt" }), 916 | ("/get/a/abc/", "/get/{param}/abc/", p! { "param" => "a" }), 917 | ("/get/t/abc/", "/get/{param}/abc/", p! { "param" => "t" }), 918 | ("/get/aa/abc/", "/get/{param}/abc/", p! { "param" => "aa" }), 919 | ( 920 | "/get/abas/abc/", 921 | "/get/{param}/abc/", 922 | p! { "param" => "abas" }, 923 | ), 924 | ( 925 | "/something/secondthing/test", 926 | "/something/secondthing/test", 927 | p! {}, 928 | ), 929 | ( 930 | "/something/abcdad/thirdthing", 931 | "/something/{paramname}/thirdthing", 932 | p! { "paramname" => "abcdad" }, 933 | ), 934 | ( 935 | "/something/secondthingaaaa/thirdthing", 936 | "/something/{paramname}/thirdthing", 937 | p! { "paramname" => "secondthingaaaa" }, 938 | ), 939 | ( 940 | "/something/se/thirdthing", 941 | "/something/{paramname}/thirdthing", 942 | p! { "paramname" => "se" }, 943 | ), 944 | ( 945 | "/something/s/thirdthing", 946 | "/something/{paramname}/thirdthing", 947 | p! { "paramname" => "s" }, 948 | ), 949 | ("/c/d/ee", "/{cc}/{dd}/ee", p! { "cc" => "c", "dd" => "d" }), 950 | ( 951 | "/c/d/e/ff", 952 | "/{cc}/{dd}/{ee}/ff", 953 | p! { "cc" => "c", "dd" => "d", "ee" => "e" }, 954 | ), 955 | ( 956 | "/c/d/e/f/gg", 957 | "/{cc}/{dd}/{ee}/{ff}/gg", 958 | p! { "cc" => "c", "dd" => "d", "ee" => "e", "ff" => "f" }, 959 | ), 960 | ( 961 | "/c/d/e/f/g/hh", 962 | "/{cc}/{dd}/{ee}/{ff}/{gg}/hh", 963 | p! { "cc" => "c", "dd" => "d", "ee" => "e", "ff" => "f", "gg" => "g" }, 964 | ), 965 | ( 966 | "/cc/dd/ee/ff/gg/hh", 967 | "/{cc}/{dd}/{ee}/{ff}/{gg}/hh", 968 | p! { "cc" => "cc", "dd" => "dd", "ee" => "ee", "ff" => "ff", "gg" => "gg" }, 969 | ), 970 | ("/get/abc", "/get/abc", p! {}), 971 | ("/get/a", "/get/{param}", p! { "param" => "a" }), 972 | ("/get/abz", "/get/{param}", p! { "param" => "abz" }), 973 | ("/get/12a", "/get/{param}", p! { "param" => "12a" }), 974 | ("/get/abcd", "/get/{param}", p! { "param" => "abcd" }), 975 | ("/get/abc/123abc", "/get/abc/123abc", p! {}), 976 | ("/get/abc/12", "/get/abc/{param}", p! { "param" => "12" }), 977 | ( 978 | "/get/abc/123ab", 979 | "/get/abc/{param}", 980 | p! { "param" => "123ab" }, 981 | ), 982 | ("/get/abc/xyz", "/get/abc/{param}", p! { "param" => "xyz" }), 983 | ( 984 | "/get/abc/123abcddxx", 985 | "/get/abc/{param}", 986 | p! { "param" => "123abcddxx" }, 987 | ), 988 | ("/get/abc/123abc/xxx8", "/get/abc/123abc/xxx8", p! {}), 989 | ( 990 | "/get/abc/123abc/x", 991 | "/get/abc/123abc/{param}", 992 | p! { "param" => "x" }, 993 | ), 994 | ( 995 | "/get/abc/123abc/xxx", 996 | "/get/abc/123abc/{param}", 997 | p! { "param" => "xxx" }, 998 | ), 999 | ( 1000 | "/get/abc/123abc/abc", 1001 | "/get/abc/123abc/{param}", 1002 | p! { "param" => "abc" }, 1003 | ), 1004 | ( 1005 | "/get/abc/123abc/xxx8xxas", 1006 | "/get/abc/123abc/{param}", 1007 | p! { "param" => "xxx8xxas" }, 1008 | ), 1009 | ( 1010 | "/get/abc/123abc/xxx8/1234", 1011 | "/get/abc/123abc/xxx8/1234", 1012 | p! {}, 1013 | ), 1014 | ( 1015 | "/get/abc/123abc/xxx8/1", 1016 | "/get/abc/123abc/xxx8/{param}", 1017 | p! { "param" => "1" }, 1018 | ), 1019 | ( 1020 | "/get/abc/123abc/xxx8/123", 1021 | "/get/abc/123abc/xxx8/{param}", 1022 | p! { "param" => "123" }, 1023 | ), 1024 | ( 1025 | "/get/abc/123abc/xxx8/78k", 1026 | "/get/abc/123abc/xxx8/{param}", 1027 | p! { "param" => "78k" }, 1028 | ), 1029 | ( 1030 | "/get/abc/123abc/xxx8/1234xxxd", 1031 | "/get/abc/123abc/xxx8/{param}", 1032 | p! { "param" => "1234xxxd" }, 1033 | ), 1034 | ( 1035 | "/get/abc/123abc/xxx8/1234/ffas", 1036 | "/get/abc/123abc/xxx8/1234/ffas", 1037 | p! {}, 1038 | ), 1039 | ( 1040 | "/get/abc/123abc/xxx8/1234/f", 1041 | "/get/abc/123abc/xxx8/1234/{param}", 1042 | p! { "param" => "f" }, 1043 | ), 1044 | ( 1045 | "/get/abc/123abc/xxx8/1234/ffa", 1046 | "/get/abc/123abc/xxx8/1234/{param}", 1047 | p! { "param" => "ffa" }, 1048 | ), 1049 | ( 1050 | "/get/abc/123abc/xxx8/1234/kka", 1051 | "/get/abc/123abc/xxx8/1234/{param}", 1052 | p! { "param" => "kka" }, 1053 | ), 1054 | ( 1055 | "/get/abc/123abc/xxx8/1234/ffas321", 1056 | "/get/abc/123abc/xxx8/1234/{param}", 1057 | p! { "param" => "ffas321" }, 1058 | ), 1059 | ( 1060 | "/get/abc/123abc/xxx8/1234/kkdd/12c", 1061 | "/get/abc/123abc/xxx8/1234/kkdd/12c", 1062 | p! {}, 1063 | ), 1064 | ( 1065 | "/get/abc/123abc/xxx8/1234/kkdd/1", 1066 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1067 | p! { "param" => "1" }, 1068 | ), 1069 | ( 1070 | "/get/abc/123abc/xxx8/1234/kkdd/12", 1071 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1072 | p! { "param" => "12" }, 1073 | ), 1074 | ( 1075 | "/get/abc/123abc/xxx8/1234/kkdd/12b", 1076 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1077 | p! { "param" => "12b" }, 1078 | ), 1079 | ( 1080 | "/get/abc/123abc/xxx8/1234/kkdd/34", 1081 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1082 | p! { "param" => "34" }, 1083 | ), 1084 | ( 1085 | "/get/abc/123abc/xxx8/1234/kkdd/12c2e3", 1086 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1087 | p! { "param" => "12c2e3" }, 1088 | ), 1089 | ( 1090 | "/get/abc/12/test", 1091 | "/get/abc/{param}/test", 1092 | p! { "param" => "12" }, 1093 | ), 1094 | ( 1095 | "/get/abc/123abdd/test", 1096 | "/get/abc/{param}/test", 1097 | p! { "param" => "123abdd" }, 1098 | ), 1099 | ( 1100 | "/get/abc/123abdddf/test", 1101 | "/get/abc/{param}/test", 1102 | p! { "param" => "123abdddf" }, 1103 | ), 1104 | ( 1105 | "/get/abc/123ab/test", 1106 | "/get/abc/{param}/test", 1107 | p! { "param" => "123ab" }, 1108 | ), 1109 | ( 1110 | "/get/abc/123abgg/test", 1111 | "/get/abc/{param}/test", 1112 | p! { "param" => "123abgg" }, 1113 | ), 1114 | ( 1115 | "/get/abc/123abff/test", 1116 | "/get/abc/{param}/test", 1117 | p! { "param" => "123abff" }, 1118 | ), 1119 | ( 1120 | "/get/abc/123abffff/test", 1121 | "/get/abc/{param}/test", 1122 | p! { "param" => "123abffff" }, 1123 | ), 1124 | ( 1125 | "/get/abc/123abd/test", 1126 | "/get/abc/123abd/{param}", 1127 | p! { "param" => "test" }, 1128 | ), 1129 | ( 1130 | "/get/abc/123abddd/test", 1131 | "/get/abc/123abddd/{param}", 1132 | p! { "param" => "test" }, 1133 | ), 1134 | ( 1135 | "/get/abc/123/test22", 1136 | "/get/abc/123/{param}", 1137 | p! { "param" => "test22" }, 1138 | ), 1139 | ( 1140 | "/get/abc/123abg/test", 1141 | "/get/abc/123abg/{param}", 1142 | p! { "param" => "test" }, 1143 | ), 1144 | ( 1145 | "/get/abc/123abf/testss", 1146 | "/get/abc/123abf/{param}", 1147 | p! { "param" => "testss" }, 1148 | ), 1149 | ( 1150 | "/get/abc/123abfff/te", 1151 | "/get/abc/123abfff/{param}", 1152 | p! { "param" => "te" }, 1153 | ), 1154 | ], 1155 | } 1156 | .run() 1157 | } 1158 | -------------------------------------------------------------------------------- /tests/merge.rs: -------------------------------------------------------------------------------- 1 | use matchit::{InsertError, Router}; 2 | 3 | #[test] 4 | fn merge_ok() { 5 | let mut root = Router::new(); 6 | assert!(root.insert("/foo", "foo").is_ok()); 7 | assert!(root.insert("/bar/{id}", "bar").is_ok()); 8 | 9 | let mut child = Router::new(); 10 | assert!(child.insert("/baz", "baz").is_ok()); 11 | assert!(child.insert("/xyz/{id}", "xyz").is_ok()); 12 | 13 | assert!(root.merge(child).is_ok()); 14 | 15 | assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); 16 | assert_eq!(root.at("/bar/1").map(|m| *m.value), Ok("bar")); 17 | assert_eq!(root.at("/baz").map(|m| *m.value), Ok("baz")); 18 | assert_eq!(root.at("/xyz/2").map(|m| *m.value), Ok("xyz")); 19 | } 20 | 21 | #[test] 22 | fn merge_conflict() { 23 | let mut root = Router::new(); 24 | assert!(root.insert("/foo", "foo").is_ok()); 25 | assert!(root.insert("/bar", "bar").is_ok()); 26 | 27 | let mut child = Router::new(); 28 | assert!(child.insert("/foo", "changed").is_ok()); 29 | assert!(child.insert("/bar", "changed").is_ok()); 30 | assert!(child.insert("/baz", "baz").is_ok()); 31 | 32 | let errors = root.merge(child).unwrap_err(); 33 | 34 | assert_eq!( 35 | errors.get(0), 36 | Some(&InsertError::Conflict { 37 | with: "/foo".into() 38 | }) 39 | ); 40 | 41 | assert_eq!( 42 | errors.get(1), 43 | Some(&InsertError::Conflict { 44 | with: "/bar".into() 45 | }) 46 | ); 47 | 48 | assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); 49 | assert_eq!(root.at("/bar").map(|m| *m.value), Ok("bar")); 50 | assert_eq!(root.at("/baz").map(|m| *m.value), Ok("baz")); 51 | } 52 | 53 | #[test] 54 | fn merge_nested() { 55 | let mut root = Router::new(); 56 | assert!(root.insert("/foo", "foo").is_ok()); 57 | 58 | let mut child = Router::new(); 59 | assert!(child.insert("/foo/bar", "bar").is_ok()); 60 | 61 | assert!(root.merge(child).is_ok()); 62 | 63 | assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); 64 | assert_eq!(root.at("/foo/bar").map(|m| *m.value), Ok("bar")); 65 | } 66 | -------------------------------------------------------------------------------- /tests/remove.rs: -------------------------------------------------------------------------------- 1 | use matchit::Router; 2 | 3 | struct RemoveTest { 4 | routes: Vec<&'static str>, 5 | ops: Vec<(Operation, &'static str, Option<&'static str>)>, 6 | remaining: Vec<&'static str>, 7 | } 8 | 9 | enum Operation { 10 | Insert, 11 | Remove, 12 | } 13 | 14 | use Operation::*; 15 | 16 | impl RemoveTest { 17 | fn run(self) { 18 | let mut router = Router::new(); 19 | 20 | for route in self.routes.iter() { 21 | assert_eq!(router.insert(*route, route.to_owned()), Ok(()), "{route}"); 22 | } 23 | 24 | for (op, route, expected) in self.ops.iter() { 25 | match op { 26 | Insert => { 27 | assert_eq!(router.insert(*route, route), Ok(()), "{route}") 28 | } 29 | Remove => { 30 | assert_eq!(router.remove(*route), *expected, "removing {route}",) 31 | } 32 | } 33 | } 34 | 35 | for route in self.remaining { 36 | assert!(matches!(router.at(route), Ok(_)), "remaining {route}"); 37 | } 38 | } 39 | } 40 | 41 | #[test] 42 | fn normalized() { 43 | RemoveTest { 44 | routes: vec![ 45 | "/x/{foo}/bar", 46 | "/x/{bar}/baz", 47 | "/{foo}/{baz}/bax", 48 | "/{foo}/{bar}/baz", 49 | "/{fod}/{baz}/{bax}/foo", 50 | "/{fod}/baz/bax/foo", 51 | "/{foo}/baz/bax", 52 | "/{bar}/{bay}/bay", 53 | "/s", 54 | "/s/s", 55 | "/s/s/s", 56 | "/s/s/s/s", 57 | "/s/s/{s}/x", 58 | "/s/s/{y}/d", 59 | ], 60 | ops: vec![ 61 | (Remove, "/x/{foo}/bar", Some("/x/{foo}/bar")), 62 | (Remove, "/x/{bar}/baz", Some("/x/{bar}/baz")), 63 | (Remove, "/{foo}/{baz}/bax", Some("/{foo}/{baz}/bax")), 64 | (Remove, "/{foo}/{bar}/baz", Some("/{foo}/{bar}/baz")), 65 | ( 66 | Remove, 67 | "/{fod}/{baz}/{bax}/foo", 68 | Some("/{fod}/{baz}/{bax}/foo"), 69 | ), 70 | (Remove, "/{fod}/baz/bax/foo", Some("/{fod}/baz/bax/foo")), 71 | (Remove, "/{foo}/baz/bax", Some("/{foo}/baz/bax")), 72 | (Remove, "/{bar}/{bay}/bay", Some("/{bar}/{bay}/bay")), 73 | (Remove, "/s", Some("/s")), 74 | (Remove, "/s/s", Some("/s/s")), 75 | (Remove, "/s/s/s", Some("/s/s/s")), 76 | (Remove, "/s/s/s/s", Some("/s/s/s/s")), 77 | (Remove, "/s/s/{s}/x", Some("/s/s/{s}/x")), 78 | (Remove, "/s/s/{y}/d", Some("/s/s/{y}/d")), 79 | ], 80 | remaining: vec![], 81 | } 82 | .run(); 83 | } 84 | 85 | #[test] 86 | fn test() { 87 | RemoveTest { 88 | routes: vec!["/home", "/home/{id}"], 89 | ops: vec![ 90 | (Remove, "/home", Some("/home")), 91 | (Remove, "/home", None), 92 | (Remove, "/home/{id}", Some("/home/{id}")), 93 | (Remove, "/home/{id}", None), 94 | ], 95 | remaining: vec![], 96 | } 97 | .run(); 98 | } 99 | 100 | #[test] 101 | fn blog() { 102 | RemoveTest { 103 | routes: vec![ 104 | "/{page}", 105 | "/posts/{year}/{month}/{post}", 106 | "/posts/{year}/{month}/index", 107 | "/posts/{year}/top", 108 | "/static/{*path}", 109 | "/favicon.ico", 110 | ], 111 | ops: vec![ 112 | (Remove, "/{page}", Some("/{page}")), 113 | ( 114 | Remove, 115 | "/posts/{year}/{month}/{post}", 116 | Some("/posts/{year}/{month}/{post}"), 117 | ), 118 | ( 119 | Remove, 120 | "/posts/{year}/{month}/index", 121 | Some("/posts/{year}/{month}/index"), 122 | ), 123 | (Remove, "/posts/{year}/top", Some("/posts/{year}/top")), 124 | (Remove, "/static/{*path}", Some("/static/{*path}")), 125 | (Remove, "/favicon.ico", Some("/favicon.ico")), 126 | ], 127 | remaining: vec![], 128 | } 129 | .run() 130 | } 131 | 132 | #[test] 133 | fn catchall() { 134 | RemoveTest { 135 | routes: vec!["/foo/{*catchall}", "/bar", "/bar/", "/bar/{*catchall}"], 136 | ops: vec![ 137 | (Remove, "/foo/{catchall}", None), 138 | (Remove, "/foo/{*catchall}", Some("/foo/{*catchall}")), 139 | (Remove, "/bar/", Some("/bar/")), 140 | (Insert, "/foo/*catchall", Some("/foo/*catchall")), 141 | (Remove, "/bar/{*catchall}", Some("/bar/{*catchall}")), 142 | ], 143 | remaining: vec!["/bar", "/foo/*catchall"], 144 | } 145 | .run(); 146 | } 147 | 148 | #[test] 149 | fn overlapping_routes() { 150 | RemoveTest { 151 | routes: vec![ 152 | "/home", 153 | "/home/{id}", 154 | "/users", 155 | "/users/{id}", 156 | "/users/{id}/posts", 157 | "/users/{id}/posts/{post_id}", 158 | "/articles", 159 | "/articles/{category}", 160 | "/articles/{category}/{id}", 161 | ], 162 | ops: vec![ 163 | (Remove, "/home", Some("/home")), 164 | (Insert, "/home", Some("/home")), 165 | (Remove, "/home/{id}", Some("/home/{id}")), 166 | (Insert, "/home/{id}", Some("/home/{id}")), 167 | (Remove, "/users", Some("/users")), 168 | (Insert, "/users", Some("/users")), 169 | (Remove, "/users/{id}", Some("/users/{id}")), 170 | (Insert, "/users/{id}", Some("/users/{id}")), 171 | (Remove, "/users/{id}/posts", Some("/users/{id}/posts")), 172 | (Insert, "/users/{id}/posts", Some("/users/{id}/posts")), 173 | ( 174 | Remove, 175 | "/users/{id}/posts/{post_id}", 176 | Some("/users/{id}/posts/{post_id}"), 177 | ), 178 | ( 179 | Insert, 180 | "/users/{id}/posts/{post_id}", 181 | Some("/users/{id}/posts/{post_id}"), 182 | ), 183 | (Remove, "/articles", Some("/articles")), 184 | (Insert, "/articles", Some("/articles")), 185 | (Remove, "/articles/{category}", Some("/articles/{category}")), 186 | (Insert, "/articles/{category}", Some("/articles/{category}")), 187 | ( 188 | Remove, 189 | "/articles/{category}/{id}", 190 | Some("/articles/{category}/{id}"), 191 | ), 192 | ( 193 | Insert, 194 | "/articles/{category}/{id}", 195 | Some("/articles/{category}/{id}"), 196 | ), 197 | ], 198 | remaining: vec![ 199 | "/home", 200 | "/home/{id}", 201 | "/users", 202 | "/users/{id}", 203 | "/users/{id}/posts", 204 | "/users/{id}/posts/{post_id}", 205 | "/articles", 206 | "/articles/{category}", 207 | "/articles/{category}/{id}", 208 | ], 209 | } 210 | .run(); 211 | } 212 | 213 | #[test] 214 | fn trailing_slash() { 215 | RemoveTest { 216 | routes: vec!["/{home}/", "/foo"], 217 | ops: vec![ 218 | (Remove, "/", None), 219 | (Remove, "/{home}", None), 220 | (Remove, "/foo/", None), 221 | (Remove, "/foo", Some("/foo")), 222 | (Remove, "/{home}", None), 223 | (Remove, "/{home}/", Some("/{home}/")), 224 | ], 225 | remaining: vec![], 226 | } 227 | .run(); 228 | } 229 | 230 | #[test] 231 | fn remove_root() { 232 | RemoveTest { 233 | routes: vec!["/"], 234 | ops: vec![(Remove, "/", Some("/"))], 235 | remaining: vec![], 236 | } 237 | .run(); 238 | } 239 | 240 | #[test] 241 | fn check_escaped_params() { 242 | RemoveTest { 243 | routes: vec![ 244 | "/foo/{id}", 245 | "/foo/{id}/bar", 246 | "/bar/{user}/{id}", 247 | "/bar/{user}/{id}/baz", 248 | "/baz/{product}/{user}/{id}", 249 | ], 250 | ops: vec![ 251 | (Remove, "/foo/{a}", None), 252 | (Remove, "/foo/{a}/bar", None), 253 | (Remove, "/bar/{a}/{b}", None), 254 | (Remove, "/bar/{a}/{b}/baz", None), 255 | (Remove, "/baz/{a}/{b}/{c}", None), 256 | ], 257 | remaining: vec![ 258 | "/foo/{id}", 259 | "/foo/{id}/bar", 260 | "/bar/{user}/{id}", 261 | "/bar/{user}/{id}/baz", 262 | "/baz/{product}/{user}/{id}", 263 | ], 264 | } 265 | .run(); 266 | } 267 | 268 | #[test] 269 | fn wildcard_suffix() { 270 | RemoveTest { 271 | routes: vec![ 272 | "/foo/{id}", 273 | "/foo/{id}/bar", 274 | "/foo/{id}bar", 275 | "/foo/{id}bar/baz", 276 | "/foo/{id}bar/baz/bax", 277 | "/bar/x{id}y", 278 | "/bar/x{id}y/", 279 | "/baz/x{id}y", 280 | "/baz/x{id}y/", 281 | ], 282 | ops: vec![ 283 | (Remove, "/foo/{id}", Some("/foo/{id}")), 284 | (Remove, "/foo/{id}bar", Some("/foo/{id}bar")), 285 | (Remove, "/foo/{id}bar/baz", Some("/foo/{id}bar/baz")), 286 | (Insert, "/foo/{id}bax", Some("/foo/{id}bax")), 287 | (Insert, "/foo/{id}bax/baz", Some("/foo/{id}bax/baz")), 288 | (Remove, "/foo/{id}bax/baz", Some("/foo/{id}bax/baz")), 289 | (Remove, "/bar/x{id}y", Some("/bar/x{id}y")), 290 | (Remove, "/baz/x{id}y/", Some("/baz/x{id}y/")), 291 | ], 292 | remaining: vec![ 293 | "/foo/{id}/bar", 294 | "/foo/{id}bar/baz/bax", 295 | "/foo/{id}bax", 296 | "/bar/x{id}y/", 297 | "/baz/x{id}y", 298 | ], 299 | } 300 | .run(); 301 | } 302 | --------------------------------------------------------------------------------