From 0016d734b8b77336f53271a1799ff28032c1dd42 Mon Sep 17 00:00:00 2001 From: Cathal Mullan Date: Sun, 5 Jan 2025 00:11:31 +0000 Subject: [PATCH] Refactor priority logic. --- BENCHMARKING.md | 53 +++-- Cargo.lock | 4 +- README.md | 38 ++-- benches/matchit_criterion.rs | 2 +- benches/matchit_divan.rs | 2 +- benches/matchit_routes.rs | 2 +- benches/path_tree_criterion.rs | 2 +- benches/path_tree_divan.rs | 2 +- benches/path_tree_routes.rs | 2 +- examples/oci/src/router.rs | 2 +- flake.lock | 12 +- src/delete.rs | 113 +++++++--- src/display.rs | 36 +++ src/errors.rs | 3 - src/errors/delete.rs | 12 +- src/errors/search.rs | 26 --- src/find.rs | 67 ++++-- src/insert.rs | 179 ++++++++++++--- src/lib.rs | 4 +- src/node.rs | 27 ++- src/optimize.rs | 112 +++++++--- src/parser.rs | 51 ++--- src/router.rs | 58 +++-- src/search.rs | 397 ++++++++++++++++++++++++--------- src/{vec.rs => sorted.rs} | 7 +- src/state.rs | 205 +++++++++++------ tests/constraint.rs | 16 +- tests/display.rs | 14 +- tests/dynamic.rs | 30 +-- tests/escape.rs | 16 +- tests/insert.rs | 6 +- tests/optimize.rs | 12 +- tests/optional.rs | 36 +-- tests/static.rs | 72 +++--- tests/wildcard.rs | 42 ++-- 35 files changed, 1098 insertions(+), 564 deletions(-) delete mode 100644 src/errors/search.rs rename src/{vec.rs => sorted.rs} (92%) diff --git a/BENCHMARKING.md b/BENCHMARKING.md index 6c279b37..2636823e 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -8,36 +8,49 @@ Check out our [codspeed results](https://codspeed.io/DuskSystems/wayfind/benchma For all benchmarks, we convert any extracted parameters to strings. -Some routers perform this operations automatically, while others require them to be done manually. +ALl routers provide a way to get parameters as strings, but some delay the actual decoding until post-search. -We do this to try and match behaviour as best as possible. This is as close to an "apples-to-apples" comparison as we can get. +| Library | Percent Decoding | String Parameters | +|:-----------------|:----------------:|:-----------------:| +| wayfind | no | yes | +| actix-router | partial | yes | +| matchit | no | delayed | +| ntex-router | partial | yes | +| path-tree | no | delayed | +| route-recognizer | no | yes | +| routefinder | no | yes | +| xitca-router | no | yes | -## `matchit` inspired benches +### `matchit` inspired benches In a router of 130 templates, benchmark matching 4 paths. | Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | |:-----------------|----------:|------------:|-----------:|--------------:|-------------:| -| wayfind | 329.54 ns | 5 | 329 B | 5 | 329 B | -| matchit | 372.73 ns | 5 | 480 B | 5 | 512 B | -| path-tree | 435.01 ns | 5 | 480 B | 5 | 512 B | -| xitca-router | 519.06 ns | 8 | 864 B | 8 | 896 B | -| ntex-router | 2.1569 µs | 19 | 1.312 KB | 19 | 1.344 KB | -| route-recognizer | 3.0280 µs | 161 | 8.569 KB | 161 | 8.601 KB | -| routefinder | 6.1353 µs | 68 | 5.088 KB | 68 | 5.12 KB | -| actix-router | 21.199 µs | 215 | 14 KB | 215 | 14.03 KB | +| wayfind | 296.31 ns | 4 | 288 B | 4 | 288 B | +| matchit | 363.11 ns | 4 | 448 B | 4 | 448 B | +| path-tree | 397.93 ns | 4 | 448 B | 4 | 448 B | +| xitca-router | 481.52 ns | 7 | 832 B | 7 | 832 B | +| ntex-router | 1.9511 µs | 18 | 1.28 KB | 18 | 1.28 KB | +| route-recognizer | 2.7551 µs | 160 | 8.537 KB | 160 | 8.537 KB | +| routefinder | 5.7907 µs | 67 | 5.056 KB | 67 | 5.056 KB | +| actix-router | 19.619 µs | 214 | 13.96 KB | 214 | 13.96 KB | -## `path-tree` inspired benches +### `path-tree` inspired benches In a router of 320 templates, benchmark matching 80 paths. | Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | |:-----------------|----------:|------------:|-----------:|--------------:|-------------:| -| wayfind | 4.6335 µs | 60 | 3.847 KB | 60 | 3.847 KB | -| path-tree | 7.0053 µs | 60 | 8.727 KB | 60 | 8.75 KB | -| matchit | 8.1093 µs | 141 | 19.09 KB | 141 | 19.11 KB | -| xitca-router | 9.9790 µs | 210 | 26.79 KB | 210 | 26.81 KB | -| ntex-router | 34.594 µs | 202 | 20.82 KB | 202 | 20.84 KB | -| route-recognizer | 67.158 µs | 2873 | 192.9 KB | 2873 | 206.1 KB | -| routefinder | 85.597 µs | 526 | 49.68 KB | 526 | 49.71 KB | -| actix-router | 186.41 µs | 2202 | 130.1 KB | 2202 | 130.1 KB | +| wayfind | 3.9946 µs | 59 | 3.808 KB | 59 | 3.808 KB | +| path-tree | 6.3520 µs | 59 | 8.704 KB | 59 | 8.704 KB | +| matchit | 7.7859 µs | 140 | 19.07 KB | 140 | 19.07 KB | +| xitca-router | 9.4098 µs | 209 | 26.77 KB | 209 | 26.77 KB | +| ntex-router | 31.865 µs | 201 | 20.8 KB | 201 | 20.8 KB | +| route-recognizer | 62.792 µs | 2872 | 192.9 KB | 2872 | 206.1 KB | +| routefinder | 80.345 µs | 525 | 49.66 KB | 525 | 49.66 KB | +| actix-router | 174.04 µs | 2201 | 130.1 KB | 2201 | 130.1 KB | + +### `wayfind` benches + +TODO diff --git a/Cargo.lock b/Cargo.lock index ed4e5110..dd4af4b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,9 +1196,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.94" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 368e8b20..71da1194 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Real-world projects often need fancy routing capabilities, such as projects port The goal of `wayfind` is to remain competitive with the fastest libraries, while offering advanced routing features when needed. Unused features shouldn't impact performance - you only pay for what you use. +The downside of this approach is that we can't have as rich conflict detection as other routers. + ## Features ### Dynamic Routing @@ -49,12 +51,12 @@ fn main() -> Result<(), Box> { router.insert("/users/{id}", 1)?; router.insert("/users/{id}/files/{filename}.{extension}", 2)?; - let search = router.search("/users/123")?.unwrap(); + let search = router.search("/users/123").unwrap(); assert_eq!(*search.data, 1); assert_eq!(search.template, "/users/{id}"); assert_eq!(search.parameters[0], ("id", "123")); - let search = router.search("/users/123/files/my.document.pdf")?.unwrap(); + let search = router.search("/users/123/files/my.document.pdf").unwrap(); assert_eq!(*search.data, 2); assert_eq!(search.template, "/users/{id}/files/{filename}.{extension}"); assert_eq!(search.parameters[0], ("id", "123")); @@ -87,12 +89,12 @@ fn main() -> Result<(), Box> { router.insert("/files/{*slug}/delete", 1)?; router.insert("/{*catch_all}", 2)?; - let search = router.search("/files/documents/reports/annual.pdf/delete")?.unwrap(); + let search = router.search("/files/documents/reports/annual.pdf/delete").unwrap(); assert_eq!(*search.data, 1); assert_eq!(search.template, "/files/{*slug}/delete"); assert_eq!(search.parameters[0], ("slug", "documents/reports/annual.pdf")); - let search = router.search("/any/other/path")?.unwrap(); + let search = router.search("/any/other/path").unwrap(); assert_eq!(*search.data, 2); assert_eq!(search.template, "/{*catch_all}"); assert_eq!(search.parameters[0], ("catch_all", "any/other/path")); @@ -130,18 +132,18 @@ fn main() -> Result<(), Box> { router.insert("/users(/{id})", 1)?; router.insert("/files/{*slug}/{file}(.{extension})", 2)?; - let search = router.search("/users")?.unwrap(); + let search = router.search("/users").unwrap(); assert_eq!(*search.data, 1); assert_eq!(search.template, "/users(/{id})"); assert_eq!(search.expanded, Some("/users")); - let search = router.search("/users/123")?.unwrap(); + let search = router.search("/users/123").unwrap(); assert_eq!(*search.data, 1); assert_eq!(search.template, "/users(/{id})"); assert_eq!(search.expanded, Some("/users/{id}")); assert_eq!(search.parameters[0], ("id", "123")); - let search = router.search("/files/documents/folder/report.pdf")?.unwrap(); + let search = router.search("/files/documents/folder/report.pdf").unwrap(); assert_eq!(*search.data, 2); assert_eq!(search.template, "/files/{*slug}/{file}(.{extension})"); assert_eq!(search.expanded, Some("/files/{*slug}/{file}.{extension}")); @@ -149,7 +151,7 @@ fn main() -> Result<(), Box> { assert_eq!(search.parameters[1], ("file", "report")); assert_eq!(search.parameters[2], ("extension", "pdf")); - let search = router.search("/files/documents/folder/readme")?.unwrap(); + let search = router.search("/files/documents/folder/readme").unwrap(); assert_eq!(*search.data, 2); assert_eq!(search.template, "/files/{*slug}/{file}(.{extension})"); assert_eq!(search.expanded, Some("/files/{*slug}/{file}")); @@ -232,18 +234,18 @@ fn main() -> Result<(), Box> { router.insert("/v2", 1)?; router.insert("/v2/{*name:namespace}/blobs/{type}:{digest}", 2)?; - let search = router.search("/v2")?.unwrap(); + let search = router.search("/v2").unwrap(); assert_eq!(*search.data, 1); assert_eq!(search.template, "/v2"); - let search = router.search("/v2/my-org/my-repo/blobs/sha256:1234567890")?.unwrap(); + let search = router.search("/v2/my-org/my-repo/blobs/sha256:1234567890").unwrap(); assert_eq!(*search.data, 2); assert_eq!(search.template, "/v2/{*name:namespace}/blobs/{type}:{digest}"); assert_eq!(search.parameters[0], ("name", "my-org/my-repo")); assert_eq!(search.parameters[1], ("type", "sha256")); assert_eq!(search.parameters[2], ("digest", "1234567890")); - let search = router.search("/v2/invalid repo/blobs/uploads")?; + let search = router.search("/v2/invalid repo/blobs/uploads"); assert!(search.is_none()); Ok(()) @@ -334,13 +336,6 @@ fn main() -> Result<(), Box> { insta::assert_snapshot!(router, @r" / - ├─ user [*] - │ ╰─ / - │ ├─ createWithList [*] - │ ├─ log - │ │ ├─ out [*] - │ │ ╰─ in [*] - │ ╰─ {username} [*] ├─ pet [*] │ ╰─ / │ ├─ findBy @@ -353,6 +348,13 @@ fn main() -> Result<(), Box> { │ ╰─ order [*] │ ╰─ / │ ╰─ {orderId} [*] + ├─ user [*] + │ ╰─ / + │ ├─ createWithList [*] + │ ├─ log + │ │ ├─ in [*] + │ │ ╰─ out [*] + │ ╰─ {username} [*] ╰─ {*catch_all} [*] "); diff --git a/benches/matchit_criterion.rs b/benches/matchit_criterion.rs index 42ab59e1..23d6ec35 100644 --- a/benches/matchit_criterion.rs +++ b/benches/matchit_criterion.rs @@ -24,7 +24,7 @@ fn matchit_benchmark(criterion: &mut Criterion) { bencher.iter(|| { for path in black_box(paths()) { - let output = black_box(router.search(black_box(path)).unwrap().unwrap()); + let output = black_box(router.search(black_box(path)).unwrap()); let _parameters: Vec<(&str, &str)> = black_box(output.parameters.iter().map(|p| (p.0, p.1)).collect()); } diff --git a/benches/matchit_divan.rs b/benches/matchit_divan.rs index 3bdb4339..f21938c4 100644 --- a/benches/matchit_divan.rs +++ b/benches/matchit_divan.rs @@ -23,7 +23,7 @@ fn wayfind(bencher: divan::Bencher<'_, '_>) { bencher.bench(|| { for path in black_box(paths()) { - let output = black_box(router.search(black_box(path)).unwrap().unwrap()); + let output = black_box(router.search(black_box(path)).unwrap()); let _parameters: Vec<(&str, &str)> = black_box(output.parameters.iter().map(|p| (p.0, p.1)).collect()); } diff --git a/benches/matchit_routes.rs b/benches/matchit_routes.rs index a45e5a36..bae13c0f 100644 --- a/benches/matchit_routes.rs +++ b/benches/matchit_routes.rs @@ -4,7 +4,7 @@ pub fn paths() -> impl IntoIterator { "/user/repos", "/repos/rust-lang/rust/stargazers", "/orgs/rust-lang/public_members/nikomatsakis", - "/repos/rust-lang/rust/releases/1%2E51%2E0", + "/repos/rust-lang/rust/releases/1.51.0", ] } diff --git a/benches/path_tree_criterion.rs b/benches/path_tree_criterion.rs index 832f2343..5163160c 100644 --- a/benches/path_tree_criterion.rs +++ b/benches/path_tree_criterion.rs @@ -24,7 +24,7 @@ fn path_tree_benchmark(criterion: &mut Criterion) { bencher.iter(|| { for path in black_box(paths()) { - let output = black_box(router.search(black_box(path)).unwrap().unwrap()); + let output = black_box(router.search(black_box(path)).unwrap()); let _parameters: Vec<(&str, &str)> = black_box(output.parameters.iter().map(|p| (p.0, p.1)).collect()); } diff --git a/benches/path_tree_divan.rs b/benches/path_tree_divan.rs index 0cb60529..7a2abb08 100644 --- a/benches/path_tree_divan.rs +++ b/benches/path_tree_divan.rs @@ -23,7 +23,7 @@ fn wayfind(bencher: divan::Bencher<'_, '_>) { bencher.bench(|| { for path in black_box(paths()) { - let output = black_box(router.search(black_box(path)).unwrap().unwrap()); + let output = black_box(router.search(black_box(path)).unwrap()); let _parameters: Vec<(&str, &str)> = black_box(output.parameters.iter().map(|p| (p.0, p.1)).collect()); } diff --git a/benches/path_tree_routes.rs b/benches/path_tree_routes.rs index 3eab2e1e..a846346e 100644 --- a/benches/path_tree_routes.rs +++ b/benches/path_tree_routes.rs @@ -37,7 +37,7 @@ pub fn paths() -> impl IntoIterator { "/issues", "/legacy/issues/search/rust-lang/rust/987/1597", "/legacy/repos/search/1597", - "/legacy/user/email/rust%40rust-lang.org", + "/legacy/user/email/rust@rust-lang.org", "/legacy/user/search/1597", "/licenses", "/licenses/mit", diff --git a/examples/oci/src/router.rs b/examples/oci/src/router.rs index ca33d39a..b8daf584 100644 --- a/examples/oci/src/router.rs +++ b/examples/oci/src/router.rs @@ -83,7 +83,7 @@ impl<'r> AppRouter<'r> { return StatusCode::METHOD_NOT_ALLOWED.into_response(); }; - let Ok(Some(search)) = router.search(&path) else { + let Some(search) = router.search(&path) else { return StatusCode::NOT_FOUND.into_response(); }; diff --git a/flake.lock b/flake.lock index c9e78485..4518f02b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1735821806, - "narHash": "sha256-cuNapx/uQeCgeuhUhdck3JKbgpsml259sjUQnWM7zW8=", + "lastModified": 1735915915, + "narHash": "sha256-Q4HuFAvoKAIiTRZTUxJ0ZXeTC7lLfC9/dggGHNXNlCw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d6973081434f88088e5321f83ebafe9a1167c367", + "rev": "a27871180d30ebee8aa6b11bf7fef8a52f024733", "type": "github" }, "original": { @@ -29,11 +29,11 @@ ] }, "locked": { - "lastModified": 1735957590, - "narHash": "sha256-vbkoQTInunSjwx9DKebbCNBq9bim8CRj1VbS4eiABfg=", + "lastModified": 1736044260, + "narHash": "sha256-DTAr0mAd8AZwWgRtU9ZZFPz3DwNeoH/Oi/1QMSqc9YQ=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "d40e15e076bcedc5bf5498a0f8ff5f18de4bf8c3", + "rev": "c8ed24cc104ebbc218d992e208131e9f024b69f0", "type": "github" }, "original": { diff --git a/src/delete.rs b/src/delete.rs index 7822bf8e..fe1e540a 100644 --- a/src/delete.rs +++ b/src/delete.rs @@ -17,17 +17,20 @@ impl Node<'_, T, S> { if let Some(part) = template.parts.pop() { match part { Part::Static { prefix } => self.delete_static(template, &prefix), - Part::Dynamic { - name, constraint, .. - } => self.delete_dynamic(template, &name, constraint.as_ref()), - Part::Wildcard { - name, constraint, .. - } if template.parts.is_empty() => { - self.delete_end_wildcard(&name, constraint.as_ref()) + Part::DynamicConstrained { name, constraint } => { + self.delete_dynamic_constrained(template, &name, &constraint) } - Part::Wildcard { - name, constraint, .. - } => self.delete_wildcard(template, &name, constraint.as_ref()), + Part::Dynamic { name } => self.delete_dynamic(template, &name), + Part::WildcardConstrained { name, constraint } if template.parts.is_empty() => { + self.delete_end_wildcard_constrained(&name, &constraint) + } + Part::Wildcard { name } if template.parts.is_empty() => { + self.delete_end_wildcard(&name) + } + Part::WildcardConstrained { name, constraint } => { + self.delete_wildcard_constrained(template, &name, &constraint) + } + Part::Wildcard { name } => self.delete_wildcard(template, &name), } } else { let data = self.data.take()?; @@ -57,11 +60,9 @@ impl Node<'_, T, S> { }; if child.is_empty() { - // Delete empty nodes. self.static_children.remove(index); self.needs_optimization = true; } else if child.is_compressible() { - // Compress redundant nodes. let merge = child.static_children.remove(0); let mut prefix = std::mem::take(&mut child.state.prefix); @@ -77,15 +78,33 @@ impl Node<'_, T, S> { result } - fn delete_dynamic( + fn delete_dynamic_constrained( &mut self, template: &mut Template, name: &str, - constraint: Option<&String>, + constraint: &str, ) -> Option { - let index = self.dynamic_children.iter().position(|child| { - child.state.name == name && child.state.constraint.as_ref() == constraint - })?; + let index = self + .dynamic_constrained_children + .iter() + .position(|child| child.state.name == name && child.state.constraint == constraint)?; + + let child = &mut self.dynamic_constrained_children[index]; + let result = child.delete(template); + + if child.is_empty() { + self.dynamic_constrained_children.remove(index); + self.needs_optimization = true; + } + + result + } + + fn delete_dynamic(&mut self, template: &mut Template, name: &str) -> Option { + let index = self + .dynamic_children + .iter() + .position(|child| child.state.name == name)?; let child = &mut self.dynamic_children[index]; let result = child.delete(template); @@ -98,15 +117,33 @@ impl Node<'_, T, S> { result } - fn delete_wildcard( + fn delete_wildcard_constrained( &mut self, template: &mut Template, name: &str, - constraint: Option<&String>, + constraint: &str, ) -> Option { - let index = self.wildcard_children.iter().position(|child| { - child.state.name == name && child.state.constraint.as_ref() == constraint - })?; + let index = self + .wildcard_constrained_children + .iter() + .position(|child| child.state.name == name && child.state.constraint == constraint)?; + + let child = &mut self.wildcard_constrained_children[index]; + let result = child.delete(template); + + if child.is_empty() { + self.wildcard_constrained_children.remove(index); + self.needs_optimization = true; + } + + result + } + + fn delete_wildcard(&mut self, template: &mut Template, name: &str) -> Option { + let index = self + .wildcard_children + .iter() + .position(|child| child.state.name == name)?; let child = &mut self.wildcard_children[index]; let result = child.delete(template); @@ -119,10 +156,28 @@ impl Node<'_, T, S> { result } - fn delete_end_wildcard(&mut self, name: &str, constraint: Option<&String>) -> Option { - let index = self.end_wildcard_children.iter().position(|child| { - child.state.name == name && child.state.constraint.as_ref() == constraint - })?; + fn delete_end_wildcard_constrained(&mut self, name: &str, constraint: &str) -> Option { + let index = self + .end_wildcard_constrained_children + .iter() + .position(|child| child.state.name == name && child.state.constraint == constraint)?; + + let mut child = self.end_wildcard_constrained_children.remove(index); + + let data = child.data.take()?; + self.needs_optimization = true; + + match data { + NodeData::Inline { data, .. } => Some(data), + NodeData::Shared { data, .. } => Arc::try_unwrap(data).ok(), + } + } + + fn delete_end_wildcard(&mut self, name: &str) -> Option { + let index = self + .end_wildcard_children + .iter() + .position(|child| child.state.name == name)?; let mut child = self.end_wildcard_children.remove(index); @@ -138,16 +193,22 @@ impl Node<'_, T, S> { fn is_empty(&self) -> bool { self.data.is_none() && self.static_children.is_empty() + && self.dynamic_constrained_children.is_empty() && self.dynamic_children.is_empty() + && self.wildcard_constrained_children.is_empty() && self.wildcard_children.is_empty() + && self.end_wildcard_constrained_children.is_empty() && self.end_wildcard_children.is_empty() } fn is_compressible(&self) -> bool { self.data.is_none() && self.static_children.len() == 1 + && self.dynamic_constrained_children.is_empty() && self.dynamic_children.is_empty() + && self.wildcard_constrained_children.is_empty() && self.wildcard_children.is_empty() + && self.end_wildcard_constrained_children.is_empty() && self.end_wildcard_children.is_empty() } } diff --git a/src/display.rs b/src/display.rs index 8ed1d77f..29a8ccef 100644 --- a/src/display.rs +++ b/src/display.rs @@ -37,8 +37,11 @@ impl Display for Node<'_, T, S> { }; let mut total_children = node.static_children.len() + + node.dynamic_constrained_children.len() + node.dynamic_children.len() + + node.wildcard_constrained_children.len() + node.wildcard_children.len() + + node.end_wildcard_constrained_children.len() + node.end_wildcard_children.len(); for child in node.static_children.iter() { @@ -46,16 +49,31 @@ impl Display for Node<'_, T, S> { debug_node(output, child, &new_prefix, false, total_children == 0)?; } + for child in node.dynamic_constrained_children.iter() { + total_children -= 1; + debug_node(output, child, &new_prefix, false, total_children == 0)?; + } + for child in node.dynamic_children.iter() { total_children -= 1; debug_node(output, child, &new_prefix, false, total_children == 0)?; } + for child in node.wildcard_constrained_children.iter() { + total_children -= 1; + debug_node(output, child, &new_prefix, false, total_children == 0)?; + } + for child in node.wildcard_children.iter() { total_children -= 1; debug_node(output, child, &new_prefix, false, total_children == 0)?; } + for child in node.end_wildcard_constrained_children.iter() { + total_children -= 1; + debug_node(output, child, &new_prefix, false, total_children == 0)?; + } + for child in node.end_wildcard_children.iter() { total_children -= 1; debug_node(output, child, &new_prefix, false, total_children == 0)?; @@ -70,8 +88,11 @@ impl Display for Node<'_, T, S> { // Handle root node manually if self.state.key().is_empty() { let total_children = self.static_children.len() + + self.dynamic_constrained_children.len() + self.dynamic_children.len() + + self.wildcard_constrained_children.len() + self.wildcard_children.len() + + self.end_wildcard_constrained_children.len() + self.end_wildcard_children.len(); let mut remaining = total_children; @@ -81,16 +102,31 @@ impl Display for Node<'_, T, S> { debug_node(&mut output, child, "", true, remaining == 0)?; } + for child in self.dynamic_constrained_children.iter() { + remaining -= 1; + debug_node(&mut output, child, "", true, remaining == 0)?; + } + for child in self.dynamic_children.iter() { remaining -= 1; debug_node(&mut output, child, "", true, remaining == 0)?; } + for child in self.wildcard_constrained_children.iter() { + remaining -= 1; + debug_node(&mut output, child, "", true, remaining == 0)?; + } + for child in self.wildcard_children.iter() { remaining -= 1; debug_node(&mut output, child, "", true, remaining == 0)?; } + for child in self.end_wildcard_constrained_children.iter() { + remaining -= 1; + debug_node(&mut output, child, "", true, remaining == 0)?; + } + for child in self.end_wildcard_children.iter() { remaining -= 1; debug_node(&mut output, child, "", true, remaining == 0)?; diff --git a/src/errors.rs b/src/errors.rs index 0a8bcada..9286ad40 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -10,8 +10,5 @@ pub use encoding::EncodingError; pub mod insert; pub use insert::InsertError; -pub mod search; -pub use search::SearchError; - pub mod template; pub use template::TemplateError; diff --git a/src/errors/delete.rs b/src/errors/delete.rs index 880cb3d9..c97db7bb 100644 --- a/src/errors/delete.rs +++ b/src/errors/delete.rs @@ -1,13 +1,10 @@ use std::{error::Error, fmt::Display}; -use super::{EncodingError, TemplateError}; +use super::TemplateError; /// Errors relating to attempting to delete a template from a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] pub enum DeleteError { - /// A [`EncodingError`] that occurred during the decoding. - Encoding(EncodingError), - /// A [`TemplateError`] that occurred during the delete. Template(TemplateError), @@ -73,7 +70,6 @@ impl Error for DeleteError {} impl Display for DeleteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Encoding(error) => error.fmt(f), Self::Template(error) => error.fmt(f), Self::NotFound { template } => write!( f, @@ -96,12 +92,6 @@ The template must be deleted using the same format as was inserted" } } -impl From for DeleteError { - fn from(error: EncodingError) -> Self { - Self::Encoding(error) - } -} - impl From for DeleteError { fn from(error: TemplateError) -> Self { Self::Template(error) diff --git a/src/errors/search.rs b/src/errors/search.rs deleted file mode 100644 index 2870629f..00000000 --- a/src/errors/search.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::{error::Error, fmt::Display}; - -use super::EncodingError; - -/// Errors relating to attempting to search for a path in a [`Router`](crate::Router). -#[derive(Debug, PartialEq, Eq)] -pub enum SearchError { - /// A [`EncodingError`] that occurred during the search. - Encoding(EncodingError), -} - -impl Error for SearchError {} - -impl Display for SearchError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Encoding(error) => error.fmt(f), - } - } -} - -impl From for SearchError { - fn from(error: EncodingError) -> Self { - Self::Encoding(error) - } -} diff --git a/src/find.rs b/src/find.rs index a10ac4a8..8acf043c 100644 --- a/src/find.rs +++ b/src/find.rs @@ -13,15 +13,20 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { if let Some(part) = template.parts.pop() { return match part { Part::Static { prefix } => self.find_static(template, &prefix), - Part::Dynamic { name, constraint } => { - self.find_dynamic(template, &name, constraint.as_deref()) + Part::DynamicConstrained { name, constraint } => { + self.find_dynamic_constrained(template, &name, &constraint) } - Part::Wildcard { name, constraint } if template.parts.is_empty() => { - self.find_end_wildcard(template, &name, constraint.as_deref()) + Part::Dynamic { name } => self.find_dynamic(template, &name), + Part::WildcardConstrained { name, constraint } if template.parts.is_empty() => { + self.find_end_wildcard_constrained(template, &name, &constraint) } - Part::Wildcard { name, constraint } => { - self.find_wildcard(template, &name, constraint.as_deref()) + Part::Wildcard { name } if template.parts.is_empty() => { + self.find_end_wildcard(template, &name) } + Part::WildcardConstrained { name, constraint } => { + self.find_wildcard_constrained(template, &name, &constraint) + } + Part::Wildcard { name } => self.find_wildcard(template, &name), }; } @@ -63,14 +68,39 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { None } - fn find_dynamic( + fn find_dynamic_constrained( &'r self, template: &mut Template, name: &str, - constraint: Option<&str>, + constraint: &str, ) -> Option<&'r NodeData<'r, T>> { + for child in self.dynamic_constrained_children.iter() { + if child.state.name == name && child.state.constraint == constraint { + return child.find(template); + } + } + + None + } + + fn find_dynamic(&'r self, template: &mut Template, name: &str) -> Option<&'r NodeData<'r, T>> { for child in self.dynamic_children.iter() { - if child.state.name == name && child.state.constraint.as_deref() == constraint { + if child.state.name == name { + return child.find(template); + } + } + + None + } + + fn find_end_wildcard_constrained( + &'r self, + template: &mut Template, + name: &str, + constraint: &str, + ) -> Option<&'r NodeData<'r, T>> { + for child in self.end_wildcard_constrained_children.iter() { + if child.state.name == name && child.state.constraint == constraint { return child.find(template); } } @@ -82,10 +112,9 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { &'r self, template: &mut Template, name: &str, - constraint: Option<&str>, ) -> Option<&'r NodeData<'r, T>> { for child in self.end_wildcard_children.iter() { - if child.state.name == name && child.state.constraint.as_deref() == constraint { + if child.state.name == name { return child.find(template); } } @@ -93,14 +122,24 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { None } - fn find_wildcard( + fn find_wildcard_constrained( &'r self, template: &mut Template, name: &str, - constraint: Option<&str>, + constraint: &str, ) -> Option<&'r NodeData<'r, T>> { + for child in self.wildcard_constrained_children.iter() { + if child.state.name == name && child.state.constraint == constraint { + return child.find(template); + } + } + + None + } + + fn find_wildcard(&'r self, template: &mut Template, name: &str) -> Option<&'r NodeData<'r, T>> { for child in self.wildcard_children.iter() { - if child.state.name == name && child.state.constraint.as_deref() == constraint { + if child.state.name == name { return child.find(template); } } diff --git a/src/insert.rs b/src/insert.rs index 51189ff2..ce180cfa 100644 --- a/src/insert.rs +++ b/src/insert.rs @@ -1,8 +1,11 @@ use crate::{ node::{Node, NodeData}, parser::{Part, Template}, - state::{DynamicState, EndWildcardState, NodeState, StaticState, WildcardState}, - vec::SortedNode, + sorted::SortedNode, + state::{ + DynamicConstrainedState, DynamicState, EndWildcardConstrainedState, EndWildcardState, + NodeState, StaticState, WildcardConstrainedState, WildcardState, + }, }; impl<'r, T, S: NodeState> Node<'r, T, S> { @@ -12,21 +15,20 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { if let Some(part) = template.parts.pop() { match part { Part::Static { prefix } => self.insert_static(template, data, &prefix), - Part::Dynamic { - name, constraint, .. - } => { - self.insert_dynamic(template, data, name, constraint); + Part::DynamicConstrained { name, constraint } => { + self.insert_dynamic_constrained(template, data, name, constraint); } - Part::Wildcard { - name, constraint, .. - } if template.parts.is_empty() => { - self.insert_end_wildcard(data, name, constraint); + Part::Dynamic { name } => self.insert_dynamic(template, data, name), + Part::WildcardConstrained { name, constraint } if template.parts.is_empty() => { + self.insert_end_wildcard_constrained(data, name, constraint); } - Part::Wildcard { - name, constraint, .. - } => { - self.insert_wildcard(template, data, name, constraint); + Part::Wildcard { name } if template.parts.is_empty() => { + self.insert_end_wildcard(data, name); } + Part::WildcardConstrained { name, constraint } => { + self.insert_wildcard_constrained(template, data, name, constraint); + } + Part::Wildcard { name } => self.insert_wildcard(template, data, name), }; } else { self.data = Some(data); @@ -65,13 +67,21 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { data: child.data.take(), static_children: std::mem::take(&mut child.static_children), + dynamic_constrained_children: std::mem::take( + &mut child.dynamic_constrained_children, + ), dynamic_children: std::mem::take(&mut child.dynamic_children), dynamic_children_shortcut: child.dynamic_children_shortcut, + wildcard_constrained_children: std::mem::take( + &mut child.wildcard_constrained_children, + ), wildcard_children: std::mem::take(&mut child.wildcard_children), wildcard_children_shortcut: child.wildcard_children_shortcut, + end_wildcard_constrained_children: std::mem::take( + &mut child.end_wildcard_constrained_children, + ), end_wildcard_children: std::mem::take(&mut child.end_wildcard_children), - priority: child.priority, needs_optimization: child.needs_optimization, }; @@ -80,13 +90,15 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { data: None, static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), dynamic_children: SortedNode::default(), dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), wildcard_children: SortedNode::default(), wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), end_wildcard_children: SortedNode::default(), - priority: 0, needs_optimization: false, }; @@ -111,13 +123,15 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { data: None, static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), dynamic_children: SortedNode::default(), dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), wildcard_children: SortedNode::default(), wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), end_wildcard_children: SortedNode::default(), - priority: 0, needs_optimization: false, }; @@ -128,32 +142,67 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { self.needs_optimization = true; } - fn insert_dynamic( + fn insert_dynamic_constrained( &mut self, template: &mut Template, data: NodeData<'r, T>, name: String, - constraint: Option, + constraint: String, ) { if let Some(child) = self - .dynamic_children + .dynamic_constrained_children .find_mut(|child| child.state.name == name && child.state.constraint == constraint) { child.insert(template, data); + } else { + self.dynamic_constrained_children.push({ + let mut new_child = Node { + state: DynamicConstrainedState::new(name, constraint), + data: None, + + static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), + dynamic_children: SortedNode::default(), + dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), + wildcard_children: SortedNode::default(), + wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), + end_wildcard_children: SortedNode::default(), + + needs_optimization: false, + }; + + new_child.insert(template, data); + new_child + }); + } + + self.needs_optimization = true; + } + + fn insert_dynamic(&mut self, template: &mut Template, data: NodeData<'r, T>, name: String) { + if let Some(child) = self + .dynamic_children + .find_mut(|child| child.state.name == name) + { + child.insert(template, data); } else { self.dynamic_children.push({ let mut new_child = Node { - state: DynamicState::new(name, constraint), + state: DynamicState::new(name), data: None, static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), dynamic_children: SortedNode::default(), dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), wildcard_children: SortedNode::default(), wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), end_wildcard_children: SortedNode::default(), - priority: 0, needs_optimization: false, }; @@ -165,32 +214,67 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { self.needs_optimization = true; } - fn insert_wildcard( + fn insert_wildcard_constrained( &mut self, template: &mut Template, data: NodeData<'r, T>, name: String, - constraint: Option, + constraint: String, ) { if let Some(child) = self - .wildcard_children + .wildcard_constrained_children .find_mut(|child| child.state.name == name && child.state.constraint == constraint) { child.insert(template, data); + } else { + self.wildcard_constrained_children.push({ + let mut new_child = Node { + state: WildcardConstrainedState::new(name, constraint), + data: None, + + static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), + dynamic_children: SortedNode::default(), + dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), + wildcard_children: SortedNode::default(), + wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), + end_wildcard_children: SortedNode::default(), + + needs_optimization: false, + }; + + new_child.insert(template, data); + new_child + }); + } + + self.needs_optimization = true; + } + + fn insert_wildcard(&mut self, template: &mut Template, data: NodeData<'r, T>, name: String) { + if let Some(child) = self + .wildcard_children + .find_mut(|child| child.state.name == name) + { + child.insert(template, data); } else { self.wildcard_children.push({ let mut new_child = Node { - state: WildcardState::new(name, constraint), + state: WildcardState::new(name), data: None, static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), dynamic_children: SortedNode::default(), dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), wildcard_children: SortedNode::default(), wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), end_wildcard_children: SortedNode::default(), - priority: 0, needs_optimization: false, }; @@ -202,32 +286,63 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { self.needs_optimization = true; } - fn insert_end_wildcard( + fn insert_end_wildcard_constrained( &mut self, data: NodeData<'r, T>, name: String, - constraint: Option, + constraint: String, ) { if self - .end_wildcard_children + .end_wildcard_constrained_children .iter() .any(|child| child.state.name == name && child.state.constraint == constraint) { return; } + self.end_wildcard_constrained_children.push(Node { + state: EndWildcardConstrainedState::new(name, constraint), + data: Some(data), + + static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), + dynamic_children: SortedNode::default(), + dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), + wildcard_children: SortedNode::default(), + wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), + end_wildcard_children: SortedNode::default(), + + needs_optimization: false, + }); + + self.needs_optimization = true; + } + + fn insert_end_wildcard(&mut self, data: NodeData<'r, T>, name: String) { + if self + .end_wildcard_children + .iter() + .any(|child| child.state.name == name) + { + return; + } + self.end_wildcard_children.push(Node { - state: EndWildcardState::new(name, constraint), + state: EndWildcardState::new(name), data: Some(data), static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), dynamic_children: SortedNode::default(), dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), wildcard_children: SortedNode::default(), wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), end_wildcard_children: SortedNode::default(), - priority: 0, needs_optimization: false, }); diff --git a/src/lib.rs b/src/lib.rs index adb10ab4..c66ecb39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,6 @@ pub use router::{Match, Parameters, Router}; mod search; -mod state; +mod sorted; -mod vec; +mod state; diff --git a/src/node.rs b/src/node.rs index 92d39913..1e1df78a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,8 +1,11 @@ use std::sync::Arc; use crate::{ - state::{DynamicState, EndWildcardState, NodeState, StaticState, WildcardState}, - vec::SortedNode, + sorted::SortedNode, + state::{ + DynamicConstrainedState, DynamicState, EndWildcardConstrainedState, EndWildcardState, + NodeState, StaticState, WildcardConstrainedState, WildcardState, + }, }; #[derive(Clone, Debug, Eq, PartialEq)] @@ -14,6 +17,9 @@ pub enum NodeData<'r, T> { /// The original template. template: &'r str, + + /// How 'specific' of a match this data is, based on the template complexity. + specificity: usize, }, /// Data is shared between 2 or more nodes. @@ -26,6 +32,9 @@ pub enum NodeData<'r, T> { /// The expanded template. expanded: Arc, + + /// How 'specific' of a match this data is, based on the expanded template complexity. + specificity: usize, }, } @@ -36,13 +45,10 @@ impl NodeData<'_, T> { } } - pub fn priority(&self) -> usize { + pub const fn specificity(&self) -> usize { match self { - NodeData::Inline { template, .. } => { - template.len() + (template.bytes().filter(|&b| b == b'/').count() * 100) - } - NodeData::Shared { expanded, .. } => { - expanded.len() + (expanded.bytes().filter(|&b| b == b'/').count() * 100) + NodeData::Inline { specificity, .. } | NodeData::Shared { specificity, .. } => { + *specificity } } } @@ -58,14 +64,15 @@ pub struct Node<'r, T, S: NodeState> { pub data: Option>, pub static_children: SortedNode<'r, T, StaticState>, + pub dynamic_constrained_children: SortedNode<'r, T, DynamicConstrainedState>, pub dynamic_children: SortedNode<'r, T, DynamicState>, pub dynamic_children_shortcut: bool, + pub wildcard_constrained_children: SortedNode<'r, T, WildcardConstrainedState>, pub wildcard_children: SortedNode<'r, T, WildcardState>, pub wildcard_children_shortcut: bool, + pub end_wildcard_constrained_children: SortedNode<'r, T, EndWildcardConstrainedState>, pub end_wildcard_children: SortedNode<'r, T, EndWildcardState>, - /// Higher values indicate more specific matches. - pub priority: usize, /// Flag indicating whether this node or its children need optimization. pub needs_optimization: bool, } diff --git a/src/optimize.rs b/src/optimize.rs index f02ba71d..aed1ed9b 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,39 +1,45 @@ -use crate::{ - node::{Node, NodeData}, - state::NodeState, -}; +use crate::{node::Node, state::NodeState}; impl Node<'_, T, S> { pub(crate) fn optimize(&mut self) { - self.optimize_inner(0); - } - - fn optimize_inner(&mut self, priority: usize) { - self.priority = priority + self.calculate_priority(); - if !self.needs_optimization { return; } for child in self.static_children.iter_mut() { - child.optimize_inner(self.priority); + child.optimize(); + } + + for child in self.dynamic_constrained_children.iter_mut() { + child.optimize(); } for child in self.dynamic_children.iter_mut() { - child.optimize_inner(self.priority); + child.optimize(); + } + + for child in self.wildcard_constrained_children.iter_mut() { + child.optimize(); } for child in self.wildcard_children.iter_mut() { - child.optimize_inner(self.priority); + child.optimize(); + } + + for child in self.end_wildcard_constrained_children.iter_mut() { + child.optimize(); } for child in self.end_wildcard_children.iter_mut() { - child.optimize_inner(self.priority); + child.optimize(); } self.static_children.sort(); + self.dynamic_constrained_children.sort(); self.dynamic_children.sort(); + self.wildcard_constrained_children.sort(); self.wildcard_children.sort(); + self.end_wildcard_constrained_children.sort(); self.end_wildcard_children.sort(); self.update_dynamic_children_shortcut(); @@ -42,19 +48,38 @@ impl Node<'_, T, S> { self.needs_optimization = false; } - // TODO: I'd really like to make priority relative. - fn calculate_priority(&self) -> usize { - let mut priority = self.state.priority(); - if self.data.is_some() { - priority += 1_000; - priority += self.data.as_ref().map_or(0, NodeData::priority); - } + fn update_dynamic_children_shortcut(&mut self) { + let constrained_check = self.dynamic_constrained_children.iter().all(|child| { + // Leading slash? + if child.state.name.as_bytes().first() == Some(&b'/') { + return true; + } - priority - } + // No children? + if child.static_children.is_empty() + && child.dynamic_constrained_children.is_empty() + && child.dynamic_children.is_empty() + && child.wildcard_constrained_children.is_empty() + && child.wildcard_children.is_empty() + && child.end_wildcard_constrained_children.is_empty() + && child.end_wildcard_children.is_empty() + { + return true; + } - fn update_dynamic_children_shortcut(&mut self) { - self.dynamic_children_shortcut = self.dynamic_children.iter().all(|child| { + // All static children start with a slash? + if child + .static_children + .iter() + .all(|child| child.state.prefix.first() == Some(&b'/')) + { + return true; + } + + false + }); + + let unconstrained_check = self.dynamic_children.iter().all(|child| { // Leading slash? if child.state.name.as_bytes().first() == Some(&b'/') { return true; @@ -62,8 +87,11 @@ impl Node<'_, T, S> { // No children? if child.static_children.is_empty() + && child.dynamic_constrained_children.is_empty() && child.dynamic_children.is_empty() + && child.wildcard_constrained_children.is_empty() && child.wildcard_children.is_empty() + && child.end_wildcard_constrained_children.is_empty() && child.end_wildcard_children.is_empty() { return true; @@ -80,14 +108,44 @@ impl Node<'_, T, S> { false }); + + self.dynamic_children_shortcut = constrained_check && unconstrained_check; } fn update_wildcard_children_shortcut(&mut self) { - self.wildcard_children_shortcut = self.wildcard_children.iter().all(|child| { + let constrained_check = self.wildcard_constrained_children.iter().all(|child| { + // No children? + if child.static_children.is_empty() + && child.dynamic_constrained_children.is_empty() + && child.dynamic_children.is_empty() + && child.wildcard_constrained_children.is_empty() + && child.wildcard_children.is_empty() + && child.end_wildcard_constrained_children.is_empty() + && child.end_wildcard_children.is_empty() + { + return true; + } + + // All static children start with a slash? + if child + .static_children + .iter() + .all(|child| child.state.prefix.first() == Some(&b'/')) + { + return true; + } + + false + }); + + let unconstrained_check = self.wildcard_children.iter().all(|child| { // No children? if child.static_children.is_empty() + && child.dynamic_constrained_children.is_empty() && child.dynamic_children.is_empty() + && child.wildcard_constrained_children.is_empty() && child.wildcard_children.is_empty() + && child.end_wildcard_constrained_children.is_empty() && child.end_wildcard_children.is_empty() { return true; @@ -104,5 +162,7 @@ impl Node<'_, T, S> { false }); + + self.wildcard_children_shortcut = constrained_check && unconstrained_check; } } diff --git a/src/parser.rs b/src/parser.rs index e64117c3..26553bc0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -14,19 +14,11 @@ pub struct Template { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Part { - Static { - prefix: Vec, - }, - - Dynamic { - name: String, - constraint: Option, - }, - - Wildcard { - name: String, - constraint: Option, - }, + Static { prefix: Vec }, + DynamicConstrained { name: String, constraint: String }, + Dynamic { name: String }, + WildcardConstrained { name: String, constraint: String }, + Wildcard { name: String }, } #[derive(Debug, PartialEq, Eq)] @@ -182,7 +174,11 @@ impl ParsedTemplate { } // Check for duplicate names. - if let Part::Dynamic { name, .. } | Part::Wildcard { name, .. } = &part { + if let Part::Dynamic { name } + | Part::DynamicConstrained { name, .. } + | Part::Wildcard { name } + | Part::WildcardConstrained { name, .. } = &part + { if let Some((_, start, length)) = seen_parameters .iter() .find(|(existing, _, _)| existing == name) @@ -354,10 +350,11 @@ impl ParsedTemplate { None }; - let part = if is_wildcard { - Part::Wildcard { name, constraint } - } else { - Part::Dynamic { name, constraint } + let part = match (is_wildcard, constraint) { + (true, Some(constraint)) => Part::WildcardConstrained { name, constraint }, + (true, None) => Part::Wildcard { name }, + (false, Some(constraint)) => Part::DynamicConstrained { name, constraint }, + (false, None) => Part::Dynamic { name }, }; Ok((part, end + 1)) @@ -400,7 +397,6 @@ mod tests { parts: vec![ Part::Dynamic { name: "name".to_owned(), - constraint: None }, Part::Static { prefix: b"/".to_vec() @@ -424,7 +420,6 @@ mod tests { parts: vec![ Part::Wildcard { name: "route".to_owned(), - constraint: None }, Part::Static { prefix: b"/".to_vec() @@ -446,16 +441,16 @@ mod tests { input: b"/{*name:alpha}/{id:numeric}".to_vec(), raw: b"/{*name:alpha}/{id:numeric}".to_vec(), parts: vec![ - Part::Dynamic { + Part::DynamicConstrained { name: "id".to_owned(), - constraint: Some("numeric".to_owned()) + constraint: "numeric".to_owned() }, Part::Static { prefix: b"/".to_vec() }, - Part::Wildcard { + Part::WildcardConstrained { name: "name".to_owned(), - constraint: Some("alpha".to_owned()) + constraint: "alpha".to_owned() }, Part::Static { prefix: b"/".to_vec() @@ -480,7 +475,6 @@ mod tests { parts: vec![ Part::Dynamic { name: "id".to_owned(), - constraint: None }, Part::Static { prefix: b"/users/".to_vec() @@ -516,7 +510,6 @@ mod tests { }, Part::Dynamic { name: "id".to_owned(), - constraint: None }, Part::Static { prefix: b"/users/".to_vec() @@ -529,7 +522,6 @@ mod tests { parts: vec![ Part::Dynamic { name: "id".to_owned(), - constraint: None }, Part::Static { prefix: b"/users/".to_vec() @@ -583,7 +575,6 @@ mod tests { }, Part::Dynamic { name: "lang".to_owned(), - constraint: None }, Part::Static { prefix: b"/".to_vec() @@ -616,14 +607,12 @@ mod tests { parts: vec![ Part::Dynamic { name: "page".to_owned(), - constraint: None }, Part::Static { prefix: b"/".to_vec() }, Part::Dynamic { name: "lang".to_owned(), - constraint: None }, Part::Static { prefix: b"/".to_vec() @@ -636,7 +625,6 @@ mod tests { parts: vec![ Part::Dynamic { name: "lang".to_owned(), - constraint: None }, Part::Static { prefix: b"/".to_vec() @@ -649,7 +637,6 @@ mod tests { parts: vec![ Part::Dynamic { name: "page".to_owned(), - constraint: None }, Part::Static { prefix: b"/".to_vec() diff --git a/src/router.rs b/src/router.rs index 623e024f..8deaec16 100644 --- a/src/router.rs +++ b/src/router.rs @@ -9,11 +9,11 @@ use smallvec::{smallvec, SmallVec}; use crate::{ constraints::Constraint, - errors::{ConstraintError, DeleteError, InsertError, SearchError}, + errors::{ConstraintError, DeleteError, InsertError}, node::{Node, NodeData}, parser::{ParsedTemplate, Part}, + sorted::SortedNode, state::RootState, - vec::SortedNode, }; #[derive(Debug, Eq, PartialEq)] @@ -47,13 +47,15 @@ impl<'r, T> Router<'r, T> { data: None, static_children: SortedNode::default(), + dynamic_constrained_children: SortedNode::default(), dynamic_children: SortedNode::default(), dynamic_children_shortcut: false, + wildcard_constrained_children: SortedNode::default(), wildcard_children: SortedNode::default(), wildcard_children_shortcut: false, + end_wildcard_constrained_children: SortedNode::default(), end_wildcard_children: SortedNode::default(), - priority: 0, needs_optimization: false, }, constraints: HashMap::default(), @@ -106,13 +108,11 @@ impl<'r, T> Router<'r, T> { // Check for invalid constraints. for template in &parsed.templates { for part in &template.parts { - if let Part::Dynamic { - constraint: Some(name), - .. + if let Part::DynamicConstrained { + constraint: name, .. } - | Part::Wildcard { - constraint: Some(name), - .. + | Part::WildcardConstrained { + constraint: name, .. } = part { if !self.constraints.contains_key(name.as_str()) { @@ -135,18 +135,42 @@ impl<'r, T> Router<'r, T> { let data = Arc::from(data); for mut parsed_template in parsed.templates { let expanded = Arc::from(String::from_utf8_lossy(&parsed_template.raw)); + + #[allow(clippy::naive_bytecount)] + let slashes = parsed_template.raw.iter().filter(|&b| *b == b'/').count(); + let final_length = parsed_template + .raw + .rsplit(|&b| b == b'/') + .next() + .map_or(0, <[u8]>::len); + self.root.insert( &mut parsed_template, NodeData::Shared { data: Arc::clone(&data), template, expanded, + specificity: slashes + final_length, }, ); } } else if let Some(parsed_template) = parsed.templates.first_mut() { - self.root - .insert(parsed_template, NodeData::Inline { data, template }); + #[allow(clippy::naive_bytecount)] + let slashes = parsed_template.raw.iter().filter(|&b| *b == b'/').count(); + let final_length = parsed_template + .raw + .rsplit(|&b| b == b'/') + .next() + .map_or(0, <[u8]>::len); + + self.root.insert( + parsed_template, + NodeData::Inline { + data, + template, + specificity: slashes + final_length, + }, + ); }; self.root.optimize(); @@ -203,15 +227,15 @@ impl<'r, T> Router<'r, T> { Ok(data) } - pub fn search<'p>(&'r self, path: &'p str) -> Result>, SearchError> { + pub fn search<'p>(&'r self, path: &'p str) -> Option> { let mut parameters = smallvec![]; let data = match self .root - .search(path.as_bytes(), &mut parameters, &self.constraints)? + .search(path.as_bytes(), &mut parameters, &self.constraints) { - Some((data, _)) => data, + Some(data) => data, _ => { - return Ok(None); + return None; } }; @@ -225,12 +249,12 @@ impl<'r, T> Router<'r, T> { } => (data.as_ref(), template, Some(expanded.as_ref())), }; - Ok(Some(Match { + Some(Match { data, template, expanded, parameters, - })) + }) } } diff --git a/src/search.rs b/src/search.rs index 24cb3692..da471dbe 100644 --- a/src/search.rs +++ b/src/search.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use smallvec::smallvec; use crate::{ - errors::{EncodingError, SearchError}, node::{Node, NodeData}, router::{Parameters, StoredConstraint}, state::NodeState, @@ -19,28 +18,64 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { + ) -> Option<&'r NodeData<'r, T>> { if path.is_empty() { - return Ok(self.data.as_ref().map(|data| (data, self.priority))); + return self.data.as_ref(); } - if let Some(search) = self.search_static(path, parameters, constraints)? { - return Ok(Some(search)); + if let Some(search) = self.search_static(path, parameters, constraints) { + return Some(search); } - if let Some(search) = self.search_dynamic(path, parameters, constraints)? { - return Ok(Some(search)); + if let Some(search) = { + if self.dynamic_children_shortcut { + self.search_dynamic_constrained_segment(path, parameters, constraints) + } else { + self.search_dynamic_constrained_inline(path, parameters, constraints) + } + } { + return Some(search); + } + + if let Some(search) = { + if self.dynamic_children_shortcut { + self.search_dynamic_segment(path, parameters, constraints) + } else { + self.search_dynamic_inline(path, parameters, constraints) + } + } { + return Some(search); } - if let Some(search) = self.search_wildcard(path, parameters, constraints)? { - return Ok(Some(search)); + if let Some(search) = { + if self.wildcard_children_shortcut { + self.search_wildcard_constrained_segment(path, parameters, constraints) + } else { + self.search_wildcard_constrained_inline(path, parameters, constraints) + } + } { + return Some(search); } - if let Some(search) = self.search_end_wildcard(path, parameters, constraints)? { - return Ok(Some(search)); + if let Some(search) = { + if self.wildcard_children_shortcut { + self.search_wildcard_segment(path, parameters, constraints) + } else { + self.search_wildcard_inline(path, parameters, constraints) + } + } { + return Some(search); } - Ok(None) + if let Some(search) = self.search_end_wildcard_constrained(path, parameters, constraints) { + return Some(search); + } + + if let Some(search) = self.search_end_wildcard(path, parameters) { + return Some(search); + } + + None } fn search_static<'p>( @@ -48,47 +83,59 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { + ) -> Option<&'r NodeData<'r, T>> { for child in self.static_children.iter() { if path.len() >= child.state.prefix.len() && child.state.prefix.iter().zip(path).all(|(a, b)| a == b) { let remaining_path = &path[child.state.prefix.len()..]; - if let Some((data, priority)) = - child.search(remaining_path, parameters, constraints)? - { - return Ok(Some((data, priority))); + if let Some(data) = child.search(remaining_path, parameters, constraints) { + return Some(data); } } } - Ok(None) + None } - fn search_dynamic<'p>( + /// Can only handle simple dynamic templates like `/{segment}/`. + fn search_dynamic_constrained_segment<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { - if self.dynamic_children_shortcut { - self.search_dynamic_segment(path, parameters, constraints) - } else { - self.search_dynamic_inline(path, parameters, constraints) + ) -> Option<&'r NodeData<'r, T>> { + for child in self.dynamic_constrained_children.iter() { + let segment_end = path.iter().position(|&b| b == b'/').unwrap_or(path.len()); + + let segment = &path[..segment_end]; + if !Self::check_constraint(Some(&child.state.constraint), segment, constraints) { + continue; + } + + parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); + + if let Some(result) = child.search(&path[segment_end..], parameters, constraints) { + return Some(result); + } + + parameters.pop(); } + + None } - /// Can handle complex dynamic routes like `{name}.{extension}`. - fn search_dynamic_inline<'p>( + /// Can handle complex dynamic templates like `{name}.{extension}`. + fn search_dynamic_constrained_inline<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { - for child in self.dynamic_children.iter() { + ) -> Option<&'r NodeData<'r, T>> { + for child in self.dynamic_constrained_children.iter() { let mut consumed = 0; - let mut best_match: Option<(&'r NodeData<'r, T>, usize)> = None; + let mut best_match: Option<&'r NodeData<'r, T>> = None; let mut best_match_parameters = smallvec![]; while consumed < path.len() { @@ -99,21 +146,16 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { consumed += 1; let segment = &path[..consumed]; - if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { + if !Self::check_constraint(Some(&child.state.constraint), segment, constraints) { continue; } let mut current_parameters = parameters.clone(); - current_parameters.push(( - &child.state.name, - std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { - input: String::from_utf8_lossy(segment).to_string(), - })?, - )); - - let (data, priority) = - match child.search(&path[consumed..], &mut current_parameters, constraints)? { - Some((data, priority)) => (data, priority), + current_parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); + + let data = + match child.search(&path[consumed..], &mut current_parameters, constraints) { + Some(data) => data, _ => { continue; } @@ -122,99 +164,186 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { if best_match.is_none() || best_match .as_ref() - .map_or(false, |(_, best_priority)| priority >= *best_priority) + .map_or(false, |best| data.specificity() >= best.specificity()) { - best_match = Some((data, priority)); + best_match = Some(data); best_match_parameters = current_parameters; } } if let Some(result) = best_match { *parameters = best_match_parameters; - return Ok(Some(result)); + return Some(result); } } - Ok(None) + None } - /// Can only handle simple dynamic routes like `/{segment}/`. + /// Can only handle simple dynamic templates like `/{segment}/`. fn search_dynamic_segment<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { + ) -> Option<&'r NodeData<'r, T>> { for child in self.dynamic_children.iter() { let segment_end = path.iter().position(|&b| b == b'/').unwrap_or(path.len()); let segment = &path[..segment_end]; - if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { - continue; - } - parameters.push(( - &child.state.name, - std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { - input: String::from_utf8_lossy(segment).to_string(), - })?, - )); + parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); - if let Some(result) = child.search(&path[segment_end..], parameters, constraints)? { - return Ok(Some(result)); + if let Some(result) = child.search(&path[segment_end..], parameters, constraints) { + return Some(result); } parameters.pop(); } - Ok(None) + None } - fn search_wildcard<'p>( + /// Can handle complex dynamic templates like `{name}.{extension}`. + fn search_dynamic_inline<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { - if self.wildcard_children_shortcut { - self.search_wildcard_segment(path, parameters, constraints) - } else { - self.search_wildcard_inline(path, parameters, constraints) + ) -> Option<&'r NodeData<'r, T>> { + for child in self.dynamic_children.iter() { + let mut consumed = 0; + + let mut best_match: Option<&'r NodeData<'r, T>> = None; + let mut best_match_parameters = smallvec![]; + + while consumed < path.len() { + if path[consumed] == b'/' { + break; + } + + consumed += 1; + + let segment = &path[..consumed]; + + let mut current_parameters = parameters.clone(); + current_parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); + + let data = + match child.search(&path[consumed..], &mut current_parameters, constraints) { + Some(data) => data, + _ => { + continue; + } + }; + + if best_match.is_none() + || best_match + .as_ref() + .map_or(false, |best| data.specificity() >= best.specificity()) + { + best_match = Some(data); + best_match_parameters = current_parameters; + } + } + + if let Some(result) = best_match { + *parameters = best_match_parameters; + return Some(result); + } } + + None } - /// Can handle complex wildcard routes like `/{*name}.{extension}`. - fn search_wildcard_inline<'p>( + /// Can only handle simple wildcard templates like `/{*segment}/`. + fn search_wildcard_constrained_segment<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { - for child in self.wildcard_children.iter() { + ) -> Option<&'r NodeData<'r, T>> { + for child in self.wildcard_constrained_children.iter() { + let mut consumed = 0; + let mut remaining_path = path; + let mut section_end = false; + + while !remaining_path.is_empty() { + if section_end { + consumed += 1; + } + + let segment_end = remaining_path + .iter() + .position(|&b| b == b'/') + .unwrap_or(remaining_path.len()); + + if segment_end == 0 { + consumed += 1; + section_end = false; + } else { + consumed += segment_end; + section_end = true; + } + + let segment = if path[..consumed].ends_with(b"/") { + &path[..consumed - 1] + } else { + &path[..consumed] + }; + + if !Self::check_constraint(Some(&child.state.constraint), segment, constraints) { + break; + } + + parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); + + if let Some(result) = + child.search(&remaining_path[segment_end..], parameters, constraints) + { + return Some(result); + } + + parameters.pop(); + + if segment_end == remaining_path.len() { + break; + } + + remaining_path = &remaining_path[segment_end + 1..]; + } + } + + None + } + + /// Can handle complex wildcard templates like `/{*name}.{extension}`. + fn search_wildcard_constrained_inline<'p>( + &'r self, + path: &'p [u8], + parameters: &mut Parameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Option<&'r NodeData<'r, T>> { + for child in self.wildcard_constrained_children.iter() { let mut consumed = 0; - let mut best_match: Option<(&'r NodeData<'r, T>, usize)> = None; + let mut best_match: Option<&'r NodeData<'r, T>> = None; let mut best_match_parameters = smallvec![]; while consumed < path.len() { consumed += 1; let segment = &path[..consumed]; - if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { + if !Self::check_constraint(Some(&child.state.constraint), segment, constraints) { continue; } let mut current_parameters = parameters.clone(); - current_parameters.push(( - &child.state.name, - std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { - input: String::from_utf8_lossy(segment).to_string(), - })?, - )); - - let (data, priority) = - match child.search(&path[consumed..], &mut current_parameters, constraints)? { - Some((data, priority)) => (data, priority), + current_parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); + + let data = + match child.search(&path[consumed..], &mut current_parameters, constraints) { + Some(data) => data, _ => { continue; } @@ -223,29 +352,29 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { if best_match.is_none() || best_match .as_ref() - .map_or(false, |(_, best_priority)| priority >= *best_priority) + .map_or(false, |best| data.specificity() >= best.specificity()) { - best_match = Some((data, priority)); + best_match = Some(data); best_match_parameters = current_parameters; } } if let Some(result) = best_match { *parameters = best_match_parameters; - return Ok(Some(result)); + return Some(result); } } - Ok(None) + None } - /// Can only handle simple wildcard routes like `/{*segment}/`. + /// Can only handle simple wildcard templates like `/{*segment}/`. fn search_wildcard_segment<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { + ) -> Option<&'r NodeData<'r, T>> { for child in self.wildcard_children.iter() { let mut consumed = 0; let mut remaining_path = path; @@ -275,21 +404,12 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { &path[..consumed] }; - if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { - break; - } - - parameters.push(( - &child.state.name, - std::str::from_utf8(segment).map_err(|_| EncodingError::Utf8Error { - input: String::from_utf8_lossy(segment).to_string(), - })?, - )); + parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); if let Some(result) = - child.search(&remaining_path[segment_end..], parameters, constraints)? + child.search(&remaining_path[segment_end..], parameters, constraints) { - return Ok(Some(result)); + return Some(result); } parameters.pop(); @@ -302,31 +422,86 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { } } - Ok(None) + None } - fn search_end_wildcard<'p>( + /// Can handle complex wildcard templates like `/{*name}.{extension}`. + fn search_wildcard_inline<'p>( + &'r self, + path: &'p [u8], + parameters: &mut Parameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Option<&'r NodeData<'r, T>> { + for child in self.wildcard_children.iter() { + let mut consumed = 0; + + let mut best_match: Option<&'r NodeData<'r, T>> = None; + let mut best_match_parameters = smallvec![]; + + while consumed < path.len() { + consumed += 1; + + let segment = &path[..consumed]; + + let mut current_parameters = parameters.clone(); + current_parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); + + let data = + match child.search(&path[consumed..], &mut current_parameters, constraints) { + Some(data) => data, + _ => { + continue; + } + }; + + if best_match.is_none() + || best_match + .as_ref() + .map_or(false, |best| data.specificity() >= best.specificity()) + { + best_match = Some(data); + best_match_parameters = current_parameters; + } + } + + if let Some(result) = best_match { + *parameters = best_match_parameters; + return Some(result); + } + } + + None + } + + fn search_end_wildcard_constrained<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { - for child in self.end_wildcard_children.iter() { - if !Self::check_constraint(child.state.constraint.as_ref(), path, constraints) { + ) -> Option<&'r NodeData<'r, T>> { + for child in self.end_wildcard_constrained_children.iter() { + if !Self::check_constraint(Some(&child.state.constraint), path, constraints) { continue; } - parameters.push(( - &child.state.name, - std::str::from_utf8(path).map_err(|_| EncodingError::Utf8Error { - input: String::from_utf8_lossy(path).to_string(), - })?, - )); + parameters.push((&child.state.name, std::str::from_utf8(path).ok()?)); + return child.data.as_ref(); + } + + None + } - return Ok(child.data.as_ref().map(|data| (data, child.priority))); + fn search_end_wildcard<'p>( + &'r self, + path: &'p [u8], + parameters: &mut Parameters<'r, 'p>, + ) -> Option<&'r NodeData<'r, T>> { + if let Some(child) = self.end_wildcard_children.iter().next() { + parameters.push((&child.state.name, std::str::from_utf8(path).ok()?)); + return child.data.as_ref(); } - Ok(None) + None } fn check_constraint( diff --git a/src/vec.rs b/src/sorted.rs similarity index 92% rename from src/vec.rs rename to src/sorted.rs index 059d6d58..c0a904c6 100644 --- a/src/vec.rs +++ b/src/sorted.rs @@ -55,12 +55,7 @@ impl<'r, T, S: NodeState> SortedNode<'r, T, S> { return; } - self.vec.sort_by(|a, b| { - b.priority - .cmp(&a.priority) - .then_with(|| a.state.cmp(&b.state)) - }); - + self.vec.sort_by(|a, b| a.state.cmp(&b.state)); self.sorted = true; } } diff --git a/src/state.rs b/src/state.rs index ae4edd47..fa407299 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,16 +1,12 @@ use std::cmp::Ordering; -// FIXME: Consider doing more of this at parse time? - pub trait NodeState: Ord { - fn priority(&self) -> usize; fn padding(&self) -> usize; fn key(&self) -> &str; } #[derive(Clone, Debug, Eq, PartialEq)] pub struct RootState { - priority: usize, padding: usize, key: String, } @@ -19,7 +15,6 @@ impl RootState { #[must_use] pub const fn new() -> Self { Self { - priority: 0, padding: 0, key: String::new(), } @@ -27,10 +22,6 @@ impl RootState { } impl NodeState for RootState { - fn priority(&self) -> usize { - self.priority - } - fn padding(&self) -> usize { self.padding } @@ -55,7 +46,6 @@ impl PartialOrd for RootState { #[derive(Clone, Debug, Eq, PartialEq)] pub struct StaticState { pub prefix: Vec, - priority: usize, padding: usize, key: String, } @@ -63,13 +53,11 @@ pub struct StaticState { impl StaticState { #[must_use] pub fn new(prefix: Vec) -> Self { - let priority = prefix.len(); let padding = prefix.len().saturating_sub(1); let key = String::from_utf8_lossy(&prefix).into_owned(); Self { prefix, - priority, padding, key, } @@ -77,10 +65,6 @@ impl StaticState { } impl NodeState for StaticState { - fn priority(&self) -> usize { - self.priority - } - fn padding(&self) -> usize { self.padding } @@ -105,41 +89,66 @@ impl PartialOrd for StaticState { #[derive(Clone, Debug, Eq, PartialEq)] pub struct DynamicState { pub name: String, - pub constraint: Option, - priority: usize, padding: usize, key: String, } impl DynamicState { #[must_use] - pub fn new(name: String, constraint: Option) -> Self { - let mut priority = name.len(); - if constraint.is_some() { - priority += 10_000; - } + pub fn new(name: String) -> Self { + let padding = name.len().saturating_sub(1); + let key = format!("{{{name}}}"); + + Self { name, padding, key } + } +} + +impl NodeState for DynamicState { + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for DynamicState { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for DynamicState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DynamicConstrainedState { + pub name: String, + pub constraint: String, + padding: usize, + key: String, +} +impl DynamicConstrainedState { + #[must_use] + pub fn new(name: String, constraint: String) -> Self { let padding = name.len().saturating_sub(1); - let key = constraint.as_ref().map_or_else( - || format!("{{{name}}}"), - |constraint| format!("{{{name}:{constraint}}}"), - ); + let key = format!("{{{name}:{constraint}}}"); Self { name, constraint, - priority, padding, key, } } } -impl NodeState for DynamicState { - fn priority(&self) -> usize { - self.priority - } - +impl NodeState for DynamicConstrainedState { fn padding(&self) -> usize { self.padding } @@ -149,7 +158,7 @@ impl NodeState for DynamicState { } } -impl Ord for DynamicState { +impl Ord for DynamicConstrainedState { fn cmp(&self, other: &Self) -> Ordering { self.name .cmp(&other.name) @@ -157,7 +166,7 @@ impl Ord for DynamicState { } } -impl PartialOrd for DynamicState { +impl PartialOrd for DynamicConstrainedState { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } @@ -166,41 +175,66 @@ impl PartialOrd for DynamicState { #[derive(Clone, Debug, Eq, PartialEq)] pub struct WildcardState { pub name: String, - pub constraint: Option, - priority: usize, padding: usize, key: String, } impl WildcardState { #[must_use] - pub fn new(name: String, constraint: Option) -> Self { - let mut priority = name.len(); - if constraint.is_some() { - priority += 10_000; - } + pub fn new(name: String) -> Self { + let padding = name.len().saturating_sub(1); + let key = format!("{{*{name}}}"); + Self { name, padding, key } + } +} + +impl NodeState for WildcardState { + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for WildcardState { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for WildcardState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WildcardConstrainedState { + pub name: String, + pub constraint: String, + padding: usize, + key: String, +} + +impl WildcardConstrainedState { + #[must_use] + pub fn new(name: String, constraint: String) -> Self { let padding = name.len().saturating_sub(1); - let key = constraint.as_ref().map_or_else( - || format!("{{*{name}}}"), - |constraint| format!("{{*{name}:{constraint}}}"), - ); + let key = format!("{{*{name}:{constraint}}}"); Self { name, constraint, - priority, padding, key, } } } -impl NodeState for WildcardState { - fn priority(&self) -> usize { - self.priority - } - +impl NodeState for WildcardConstrainedState { fn padding(&self) -> usize { self.padding } @@ -210,7 +244,7 @@ impl NodeState for WildcardState { } } -impl Ord for WildcardState { +impl Ord for WildcardConstrainedState { fn cmp(&self, other: &Self) -> Ordering { self.name .cmp(&other.name) @@ -218,7 +252,7 @@ impl Ord for WildcardState { } } -impl PartialOrd for WildcardState { +impl PartialOrd for WildcardConstrainedState { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } @@ -227,41 +261,66 @@ impl PartialOrd for WildcardState { #[derive(Clone, Debug, Eq, PartialEq)] pub struct EndWildcardState { pub name: String, - pub constraint: Option, - priority: usize, padding: usize, key: String, } impl EndWildcardState { #[must_use] - pub fn new(name: String, constraint: Option) -> Self { - let mut priority = name.len(); - if constraint.is_some() { - priority += 10_000; - } + pub fn new(name: String) -> Self { + let padding = name.len().saturating_sub(1); + let key = format!("{{*{name}}}"); + Self { name, padding, key } + } +} + +impl NodeState for EndWildcardState { + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl PartialOrd for EndWildcardState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for EndWildcardState { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EndWildcardConstrainedState { + pub name: String, + pub constraint: String, + padding: usize, + key: String, +} + +impl EndWildcardConstrainedState { + #[must_use] + pub fn new(name: String, constraint: String) -> Self { let padding = name.len().saturating_sub(1); - let key = constraint.as_ref().map_or_else( - || format!("{{*{name}}}"), - |constraint| format!("{{*{name}:{constraint}}}"), - ); + let key = format!("{{*{name}:{constraint}}}"); Self { name, constraint, - priority, padding, key, } } } -impl NodeState for EndWildcardState { - fn priority(&self) -> usize { - self.priority - } - +impl NodeState for EndWildcardConstrainedState { fn padding(&self) -> usize { self.padding } @@ -271,7 +330,7 @@ impl NodeState for EndWildcardState { } } -impl Ord for EndWildcardState { +impl Ord for EndWildcardConstrainedState { fn cmp(&self, other: &Self) -> Ordering { self.name .cmp(&other.name) @@ -279,7 +338,7 @@ impl Ord for EndWildcardState { } } -impl PartialOrd for EndWildcardState { +impl PartialOrd for EndWildcardConstrainedState { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } diff --git a/tests/constraint.rs b/tests/constraint.rs index 728d814e..076e14a4 100644 --- a/tests/constraint.rs +++ b/tests/constraint.rs @@ -27,7 +27,7 @@ fn test_constraint_dynamic() -> Result<(), Box> { ╰─ {id:name} [*] "); - let search = router.search("/users/john123")?; + let search = router.search("/users/john123"); assert_eq!( search, Some(Match { @@ -38,7 +38,7 @@ fn test_constraint_dynamic() -> Result<(), Box> { }) ); - let search = router.search("/users/john@123")?; + let search = router.search("/users/john@123"); assert_eq!(search, None); Ok(()) @@ -55,7 +55,7 @@ fn test_constraint_wildcard() -> Result<(), Box> { ╰─ {*path:name} [*] "); - let search = router.search("/users/john/doe123")?; + let search = router.search("/users/john/doe123"); assert_eq!( search, Some(Match { @@ -66,7 +66,7 @@ fn test_constraint_wildcard() -> Result<(), Box> { }) ); - let search = router.search("/users/john@doe/123")?; + let search = router.search("/users/john@doe/123"); assert_eq!(search, None); Ok(()) @@ -131,7 +131,7 @@ fn test_constraint_builtin() -> Result<(), Box> { ╰─ {id} [*] "); - let search = router.search("/users/abc")?; + let search = router.search("/users/abc"); assert_eq!( search, Some(Match { @@ -142,7 +142,7 @@ fn test_constraint_builtin() -> Result<(), Box> { }) ); - let search = router.search("/users/123")?; + let search = router.search("/users/123"); assert_eq!( search, Some(Match { @@ -170,7 +170,7 @@ fn test_constraint_unreachable() -> Result<(), Box> { ╰─ {id:u32} [*] "); - let search = router.search("/users/123")?; + let search = router.search("/users/123"); assert_eq!( search, Some(Match { @@ -181,7 +181,7 @@ fn test_constraint_unreachable() -> Result<(), Box> { }) ); - let search = router.search("/users/abc123")?; + let search = router.search("/users/abc123"); assert_eq!( search, Some(Match { diff --git a/tests/display.rs b/tests/display.rs index 99403dd1..dab0eb9a 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -19,23 +19,23 @@ fn test_display_router() -> Result<(), Box> { / [*] ├─ api/v1 [*] │ ╰─ / [*] - ├─ users [*] - │ ╰─ / - │ ╰─ {id} [*] - │ ╰─ /profile [*] + ├─ files/ + │ ╰─ {*path} + │ ╰─ /download [*] ├─ images/ │ ╰─ {name} [*] │ ╰─ . │ ╰─ {extension} [*] - ├─ files/ - │ ╰─ {*path} - │ ╰─ /download [*] ├─ posts/ │ ╰─ {year} │ ╰─ - │ ╰─ {month} │ ╰─ - │ ╰─ {day} [*] + ├─ users [*] + │ ╰─ / + │ ╰─ {id} [*] + │ ╰─ /profile [*] ╰─ {*catch_all} [*] "); diff --git a/tests/dynamic.rs b/tests/dynamic.rs index 5e01af51..5ec72c7b 100644 --- a/tests/dynamic.rs +++ b/tests/dynamic.rs @@ -14,7 +14,7 @@ fn test_dynamic_simple() -> Result<(), Box> { ╰─ {id} [*] "); - let search = router.search("/123")?; + let search = router.search("/123"); assert_eq!( search, Some(Match { @@ -25,7 +25,7 @@ fn test_dynamic_simple() -> Result<(), Box> { }) ); - let search = router.search("/")?; + let search = router.search("/"); assert_eq!(search, None); Ok(()) @@ -47,7 +47,7 @@ fn test_dynamic_multiple() -> Result<(), Box> { ╰─ {day} [*] "); - let search = router.search("/2024")?; + let search = router.search("/2024"); assert_eq!( search, Some(Match { @@ -58,7 +58,7 @@ fn test_dynamic_multiple() -> Result<(), Box> { }) ); - let search = router.search("/2024/12")?; + let search = router.search("/2024/12"); assert_eq!( search, Some(Match { @@ -69,7 +69,7 @@ fn test_dynamic_multiple() -> Result<(), Box> { }) ); - let search = router.search("/2024/12/01")?; + let search = router.search("/2024/12/01"); assert_eq!( search, Some(Match { @@ -99,7 +99,7 @@ fn test_dynamic_inline() -> Result<(), Box> { ╰─ {day} [*] "); - let search = router.search("/2024")?; + let search = router.search("/2024"); assert_eq!( search, Some(Match { @@ -110,7 +110,7 @@ fn test_dynamic_inline() -> Result<(), Box> { }) ); - let search = router.search("/2024-12")?; + let search = router.search("/2024-12"); assert_eq!( search, Some(Match { @@ -121,7 +121,7 @@ fn test_dynamic_inline() -> Result<(), Box> { }) ); - let search = router.search("/2024-12-01")?; + let search = router.search("/2024-12-01"); assert_eq!( search, Some(Match { @@ -147,10 +147,10 @@ fn test_dynamic_greedy() -> Result<(), Box> { ╰─ {extension} [*] "); - let search = router.search("/report")?; + let search = router.search("/report"); assert_eq!(search, None); - let search = router.search("/report.pdf")?; + let search = router.search("/report.pdf"); assert_eq!( search, Some(Match { @@ -161,7 +161,7 @@ fn test_dynamic_greedy() -> Result<(), Box> { }) ); - let search = router.search("/report.final.pdf")?; + let search = router.search("/report.final.pdf"); assert_eq!( search, Some(Match { @@ -194,7 +194,7 @@ fn test_dynamic_priority() -> Result<(), Box> { ╰─ {extension} [*] "); - let search = router.search("/robots.txt")?; + let search = router.search("/robots.txt"); assert_eq!( search, Some(Match { @@ -205,7 +205,7 @@ fn test_dynamic_priority() -> Result<(), Box> { }) ); - let search = router.search("/robots.pdf")?; + let search = router.search("/robots.pdf"); assert_eq!( search, Some(Match { @@ -216,7 +216,7 @@ fn test_dynamic_priority() -> Result<(), Box> { }) ); - let search = router.search("/config.txt")?; + let search = router.search("/config.txt"); assert_eq!( search, Some(Match { @@ -227,7 +227,7 @@ fn test_dynamic_priority() -> Result<(), Box> { }) ); - let search = router.search("/config.pdf")?; + let search = router.search("/config.pdf"); assert_eq!( search, Some(Match { diff --git a/tests/escape.rs b/tests/escape.rs index 31e48dce..33ae0df9 100644 --- a/tests/escape.rs +++ b/tests/escape.rs @@ -11,7 +11,7 @@ fn test_escape_parameter() -> Result<(), Box> { insta::assert_snapshot!(router, @"/users/{id} [*]"); - let search = router.search("/users/{id}")?; + let search = router.search("/users/{id}"); assert_eq!( search, Some(Match { @@ -22,7 +22,7 @@ fn test_escape_parameter() -> Result<(), Box> { }) ); - let search = router.search("/users/123")?; + let search = router.search("/users/123"); assert_eq!(search, None); Ok(()) @@ -35,7 +35,7 @@ fn test_escape_group() -> Result<(), Box> { insta::assert_snapshot!(router, @"/(not-optional) [*]"); - let search = router.search("/(not-optional)")?; + let search = router.search("/(not-optional)"); assert_eq!( search, Some(Match { @@ -46,7 +46,7 @@ fn test_escape_group() -> Result<(), Box> { }) ); - let search = router.search("/optional")?; + let search = router.search("/optional"); assert_eq!(search, None); Ok(()) @@ -63,7 +63,7 @@ fn test_escape_nested() -> Result<(), Box> { ╰─ /{param} [*] "); - let search = router.search("/a/{param}")?; + let search = router.search("/a/{param}"); assert_eq!( search, Some(Match { @@ -74,10 +74,10 @@ fn test_escape_nested() -> Result<(), Box> { }) ); - let search = router.search("/a/value")?; + let search = router.search("/a/value"); assert_eq!(search, None); - let search = router.search("/a")?; + let search = router.search("/a"); assert_eq!( search, Some(Match { @@ -88,7 +88,7 @@ fn test_escape_nested() -> Result<(), Box> { }) ); - let search = router.search("/")?; + let search = router.search("/"); assert_eq!( search, Some(Match { diff --git a/tests/insert.rs b/tests/insert.rs index 8bc97b20..c540f702 100644 --- a/tests/insert.rs +++ b/tests/insert.rs @@ -88,9 +88,9 @@ fn test_insert_conflict_overlapping() -> Result<(), Box> { insta::assert_snapshot!(router, @r" / - ├─ x/y [*] - ╰─ a [*] - ╰─ /b [*] + ├─ a [*] + │ ╰─ /b [*] + ╰─ x/y [*] "); Ok(()) diff --git a/tests/optimize.rs b/tests/optimize.rs index d55b3695..cc55ae44 100644 --- a/tests/optimize.rs +++ b/tests/optimize.rs @@ -13,8 +13,8 @@ fn test_optimize_removal() -> Result<(), Box> { /users/ ╰─ {id} [*] ╰─ / - ├─ settings [*] - ╰─ profile [*] + ├─ profile [*] + ╰─ settings [*] "); router.delete("/users/{id}/profile")?; @@ -46,8 +46,8 @@ fn test_optimize_data() -> Result<(), Box> { /users/ ╰─ {id} [*] ╰─ / - ├─ settings [*] - ╰─ profile [*] + ├─ profile [*] + ╰─ settings [*] "); router.delete("/users/{id}")?; @@ -56,8 +56,8 @@ fn test_optimize_data() -> Result<(), Box> { /users/ ╰─ {id} ╰─ / - ├─ settings [*] - ╰─ profile [*] + ├─ profile [*] + ╰─ settings [*] "); Ok(()) diff --git a/tests/optional.rs b/tests/optional.rs index 32f3ebf2..a68633ad 100644 --- a/tests/optional.rs +++ b/tests/optional.rs @@ -16,7 +16,7 @@ fn test_optional_starting() -> Result<(), Box> { ╰─ /users [*] "); - let search = router.search("/en/users")?; + let search = router.search("/en/users"); assert_eq!( search, Some(Match { @@ -27,7 +27,7 @@ fn test_optional_starting() -> Result<(), Box> { }) ); - let search = router.search("/users")?; + let search = router.search("/users"); assert_eq!( search, Some(Match { @@ -51,7 +51,7 @@ fn test_optional_ending() -> Result<(), Box> { ╰─ / [*] "); - let search = router.search("/users")?; + let search = router.search("/users"); assert_eq!( search, Some(Match { @@ -62,7 +62,7 @@ fn test_optional_ending() -> Result<(), Box> { }) ); - let search = router.search("/users/")?; + let search = router.search("/users/"); assert_eq!( search, Some(Match { @@ -88,7 +88,7 @@ fn test_optional_nested() -> Result<(), Box> { ╰─ /c [*] "); - let search = router.search("/a/b/c")?; + let search = router.search("/a/b/c"); assert_eq!( search, Some(Match { @@ -99,7 +99,7 @@ fn test_optional_nested() -> Result<(), Box> { }) ); - let search = router.search("/a/b")?; + let search = router.search("/a/b"); assert_eq!( search, Some(Match { @@ -110,7 +110,7 @@ fn test_optional_nested() -> Result<(), Box> { }) ); - let search = router.search("/a")?; + let search = router.search("/a"); assert_eq!( search, Some(Match { @@ -121,7 +121,7 @@ fn test_optional_nested() -> Result<(), Box> { }) ); - let search = router.search("/")?; + let search = router.search("/"); assert_eq!( search, Some(Match { @@ -145,7 +145,7 @@ fn test_optional_only() -> Result<(), Box> { ╰─ test [*] "); - let search = router.search("/test")?; + let search = router.search("/test"); assert_eq!( search, Some(Match { @@ -156,7 +156,7 @@ fn test_optional_only() -> Result<(), Box> { }) ); - let search = router.search("/")?; + let search = router.search("/"); assert_eq!( search, Some(Match { @@ -187,7 +187,7 @@ fn test_optional_touching() -> Result<(), Box> { ╰─ c [*] "); - let search = router.search("/a/b/c")?; + let search = router.search("/a/b/c"); assert_eq!( search, Some(Match { @@ -198,7 +198,7 @@ fn test_optional_touching() -> Result<(), Box> { }) ); - let search = router.search("/a/b")?; + let search = router.search("/a/b"); assert_eq!( search, Some(Match { @@ -209,7 +209,7 @@ fn test_optional_touching() -> Result<(), Box> { }) ); - let search = router.search("/a/c")?; + let search = router.search("/a/c"); assert_eq!( search, Some(Match { @@ -220,7 +220,7 @@ fn test_optional_touching() -> Result<(), Box> { }) ); - let search = router.search("/a")?; + let search = router.search("/a"); assert_eq!( search, Some(Match { @@ -231,7 +231,7 @@ fn test_optional_touching() -> Result<(), Box> { }) ); - let search = router.search("/b/c")?; + let search = router.search("/b/c"); assert_eq!( search, Some(Match { @@ -242,7 +242,7 @@ fn test_optional_touching() -> Result<(), Box> { }) ); - let search = router.search("/b")?; + let search = router.search("/b"); assert_eq!( search, Some(Match { @@ -253,7 +253,7 @@ fn test_optional_touching() -> Result<(), Box> { }) ); - let search = router.search("/c")?; + let search = router.search("/c"); assert_eq!( search, Some(Match { @@ -264,7 +264,7 @@ fn test_optional_touching() -> Result<(), Box> { }) ); - let search = router.search("/")?; + let search = router.search("/"); assert_eq!( search, Some(Match { diff --git a/tests/static.rs b/tests/static.rs index e7e1d6b9..ab916be6 100644 --- a/tests/static.rs +++ b/tests/static.rs @@ -11,7 +11,7 @@ fn test_static_simple() -> Result<(), Box> { insta::assert_snapshot!(router, @"/users [*]"); - let search = router.search("/users")?; + let search = router.search("/users"); assert_eq!( search, Some(Match { @@ -22,7 +22,7 @@ fn test_static_simple() -> Result<(), Box> { }) ); - let search = router.search("/user")?; + let search = router.search("/user"); assert_eq!(search, None); Ok(()) @@ -39,7 +39,7 @@ fn test_static_overlapping() -> Result<(), Box> { ╰─ s [*] "); - let search = router.search("/user")?; + let search = router.search("/user"); assert_eq!( search, Some(Match { @@ -50,7 +50,7 @@ fn test_static_overlapping() -> Result<(), Box> { }) ); - let search = router.search("/users")?; + let search = router.search("/users"); assert_eq!( search, Some(Match { @@ -61,10 +61,10 @@ fn test_static_overlapping() -> Result<(), Box> { }) ); - let search = router.search("/use")?; + let search = router.search("/use"); assert_eq!(search, None); - let search = router.search("/userss")?; + let search = router.search("/userss"); assert_eq!(search, None); Ok(()) @@ -82,7 +82,7 @@ fn test_static_overlapping_slash() -> Result<(), Box> { ╰─ _1 [*] "); - let search = router.search("/user_1")?; + let search = router.search("/user_1"); assert_eq!( search, Some(Match { @@ -93,7 +93,7 @@ fn test_static_overlapping_slash() -> Result<(), Box> { }) ); - let search = router.search("/user/1")?; + let search = router.search("/user/1"); assert_eq!( search, Some(Match { @@ -104,10 +104,10 @@ fn test_static_overlapping_slash() -> Result<(), Box> { }) ); - let search = router.search("/user")?; + let search = router.search("/user"); assert_eq!(search, None); - let search = router.search("/users")?; + let search = router.search("/users"); assert_eq!(search, None); Ok(()) @@ -126,19 +126,19 @@ fn test_static_split_multibyte() -> Result<(), Box> { insta::assert_snapshot!(router, @r" /� - ├─ �‍👩‍� - │ ├─ � [*] - │ ╰─ � [*] - ╰─ �‍� - ├─ �‍� - │ ├─ � [*] - │ ╰─ � [*] - ╰─ �‍� - ├─ � [*] - ╰─ � [*] + ├─ �‍� + │ ├─ �‍� + │ │ ├─ � [*] + │ │ ╰─ � [*] + │ ╰─ �‍� + │ ├─ � [*] + │ ╰─ � [*] + ╰─ �‍👩‍� + ├─ � [*] + ╰─ � [*] "); - let search = router.search("/👨‍👩‍👧")?; // Family: Man, Woman, Girl + let search = router.search("/👨‍👩‍👧"); // Family: Man, Woman, Girl assert_eq!( search, Some(Match { @@ -149,7 +149,7 @@ fn test_static_split_multibyte() -> Result<(), Box> { }) ); - let search = router.search("/👨‍👩‍👦")?; // Family: Man, Woman, Boy + let search = router.search("/👨‍👩‍👦"); // Family: Man, Woman, Boy assert_eq!( search, Some(Match { @@ -160,16 +160,16 @@ fn test_static_split_multibyte() -> Result<(), Box> { }) ); - let search = router.search("/👨")?; // Man + let search = router.search("/👨"); // Man assert_eq!(search, None); - let search = router.search("/👨‍👨")?; // Man Woman + let search = router.search("/👨‍👨"); // Man Woman assert_eq!(search, None); - let search = router.search("/👨👩👧")?; // Man, Woman, Girl + let search = router.search("/👨👩👧"); // Man, Woman, Girl assert_eq!(search, None); - let search = router.search("/👨‍👨‍👧‍👦")?; // Family: Man, Woman, Girl, Boy + let search = router.search("/👨‍👨‍👧‍👦"); // Family: Man, Woman, Girl, Boy assert_eq!(search, None); Ok(()) @@ -187,7 +187,7 @@ fn test_static_case_sensitive() -> Result<(), Box> { ╰─ users [*] "); - let search = router.search("/users")?; + let search = router.search("/users"); assert_eq!( search, Some(Match { @@ -198,7 +198,7 @@ fn test_static_case_sensitive() -> Result<(), Box> { }) ); - let search = router.search("/Users")?; + let search = router.search("/Users"); assert_eq!( search, Some(Match { @@ -219,7 +219,7 @@ fn test_static_whitespace() -> Result<(), Box> { insta::assert_snapshot!(router, @"/users /items [*]"); - let search = router.search("/users /items")?; + let search = router.search("/users /items"); assert_eq!( search, Some(Match { @@ -230,7 +230,7 @@ fn test_static_whitespace() -> Result<(), Box> { }) ); - let search = router.search("/users/items")?; + let search = router.search("/users/items"); assert_eq!(search, None); Ok(()) @@ -248,7 +248,7 @@ fn test_static_duplicate_slashes() -> Result<(), Box> { ╰─ items [*] "); - let search = router.search("/users/items")?; + let search = router.search("/users/items"); assert_eq!( search, Some(Match { @@ -259,7 +259,7 @@ fn test_static_duplicate_slashes() -> Result<(), Box> { }) ); - let search = router.search("/users//items")?; + let search = router.search("/users//items"); assert_eq!( search, Some(Match { @@ -280,7 +280,7 @@ fn test_static_empty_segments() -> Result<(), Box> { insta::assert_snapshot!(router, @"/users///items [*]"); - let search = router.search("/users///items")?; + let search = router.search("/users///items"); assert_eq!( search, Some(Match { @@ -291,13 +291,13 @@ fn test_static_empty_segments() -> Result<(), Box> { }) ); - let search = router.search("/users/items")?; + let search = router.search("/users/items"); assert_eq!(search, None); - let search = router.search("/users//items")?; + let search = router.search("/users//items"); assert_eq!(search, None); - let search = router.search("/users////items")?; + let search = router.search("/users////items"); assert_eq!(search, None); Ok(()) diff --git a/tests/wildcard.rs b/tests/wildcard.rs index ef1e09ad..d7115cc8 100644 --- a/tests/wildcard.rs +++ b/tests/wildcard.rs @@ -15,7 +15,7 @@ fn test_wildcard_simple() -> Result<(), Box> { ╰─ /delete [*] "); - let search = router.search("/docs/delete")?; + let search = router.search("/docs/delete"); assert_eq!( search, Some(Match { @@ -26,7 +26,7 @@ fn test_wildcard_simple() -> Result<(), Box> { }) ); - let search = router.search("/nested/docs/folder/delete")?; + let search = router.search("/nested/docs/folder/delete"); assert_eq!( search, Some(Match { @@ -37,7 +37,7 @@ fn test_wildcard_simple() -> Result<(), Box> { }) ); - let search = router.search("/delete")?; + let search = router.search("/delete"); assert_eq!(search, None); Ok(()) @@ -56,7 +56,7 @@ fn test_wildcard_multiple() -> Result<(), Box> { ╰─ /file [*] "); - let search = router.search("/a/static/b/file")?; + let search = router.search("/a/static/b/file"); assert_eq!( search, Some(Match { @@ -67,7 +67,7 @@ fn test_wildcard_multiple() -> Result<(), Box> { }) ); - let search = router.search("/a/b/c/static/d/e/f/file")?; + let search = router.search("/a/b/c/static/d/e/f/file"); assert_eq!( search, Some(Match { @@ -92,7 +92,7 @@ fn test_wildcard_inline() -> Result<(), Box> { ╰─ .html [*] "); - let search = router.search("/page.html")?; + let search = router.search("/page.html"); assert_eq!( search, Some(Match { @@ -103,7 +103,7 @@ fn test_wildcard_inline() -> Result<(), Box> { }) ); - let search = router.search("/nested/page.html")?; + let search = router.search("/nested/page.html"); assert_eq!( search, Some(Match { @@ -114,7 +114,7 @@ fn test_wildcard_inline() -> Result<(), Box> { }) ); - let search = router.search("/.html")?; + let search = router.search("/.html"); assert_eq!(search, None); Ok(()) @@ -132,7 +132,7 @@ fn test_wildcard_greedy() -> Result<(), Box> { ╰─ {*second} [*] "); - let search = router.search("/a-b-c")?; + let search = router.search("/a-b-c"); assert_eq!( search, Some(Match { @@ -143,7 +143,7 @@ fn test_wildcard_greedy() -> Result<(), Box> { }) ); - let search = router.search("/path/to/some-file/with-multiple-hyphens")?; + let search = router.search("/path/to/some-file/with-multiple-hyphens"); assert_eq!( search, Some(Match { @@ -171,7 +171,7 @@ fn test_wildcard_empty_segments() -> Result<(), Box> { ╰─ /end [*] "); - let search = router.search("/start/middle/end")?; + let search = router.search("/start/middle/end"); assert_eq!( search, Some(Match { @@ -182,7 +182,7 @@ fn test_wildcard_empty_segments() -> Result<(), Box> { }) ); - let search = router.search("/start//middle///end")?; + let search = router.search("/start//middle///end"); assert_eq!( search, Some(Match { @@ -212,13 +212,13 @@ fn test_wildcard_priority() -> Result<(), Box> { ├─ static/ │ ├─ path [*] │ ╰─ {*rest} [*] - ├─ {*prefix} - │ ╰─ .suffix [*] - ╰─ {*path} - ╰─ /static [*] + ├─ {*path} + │ ╰─ /static [*] + ╰─ {*prefix} + ╰─ .suffix [*] "); - let search = router.search("/static/path")?; + let search = router.search("/static/path"); assert_eq!( search, Some(Match { @@ -229,7 +229,7 @@ fn test_wildcard_priority() -> Result<(), Box> { }) ); - let search = router.search("/static/some/nested/path")?; + let search = router.search("/static/some/nested/path"); assert_eq!( search, Some(Match { @@ -240,7 +240,7 @@ fn test_wildcard_priority() -> Result<(), Box> { }) ); - let search = router.search("/some/nested/path/static")?; + let search = router.search("/some/nested/path/static"); assert_eq!( search, Some(Match { @@ -251,7 +251,7 @@ fn test_wildcard_priority() -> Result<(), Box> { }) ); - let search = router.search("/prefix.some/nested/path")?; + let search = router.search("/prefix.some/nested/path"); assert_eq!( search, Some(Match { @@ -262,7 +262,7 @@ fn test_wildcard_priority() -> Result<(), Box> { }) ); - let search = router.search("/some/nested/path.suffix")?; + let search = router.search("/some/nested/path.suffix"); assert_eq!( search, Some(Match {