diff --git a/README.md b/README.md index 368e8b2..b600a2c 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. +TODO: Mention downsides of this approach. + ## 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(()) diff --git a/benches/matchit_criterion.rs b/benches/matchit_criterion.rs index 42ab59e..23d6ec3 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 3bdb433..f21938c 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 a45e5a3..bae13c0 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 832f234..5163160 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 0cb6052..7a2abb0 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 3eab2e1..a846346 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 ca33d39..b8daf58 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/src/delete.rs b/src/delete.rs index 7822bf8..fe1e540 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 8ed1d77..29a8cce 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 0a8bcad..9286ad4 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 880cb3d..c97db7b 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 2870629..0000000 --- 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 a10ac4a..8acf043 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 51189ff..b2cf334 100644 --- a/src/insert.rs +++ b/src/insert.rs @@ -1,7 +1,10 @@ use crate::{ node::{Node, NodeData}, parser::{Part, Template}, - state::{DynamicState, EndWildcardState, NodeState, StaticState, WildcardState}, + state::{ + DynamicConstrainedState, DynamicState, EndWildcardConstrainedState, EndWildcardState, + NodeState, StaticState, WildcardConstrainedState, WildcardState, + }, vec::SortedNode, }; @@ -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,10 +67,19 @@ 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, @@ -80,10 +91,13 @@ 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, @@ -111,10 +125,13 @@ 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, @@ -128,29 +145,66 @@ 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(), + + priority: 0, + 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, @@ -165,29 +219,66 @@ 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(), + + priority: 0, + 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, @@ -202,29 +293,62 @@ 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(), + + priority: 0, + 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, diff --git a/src/node.rs b/src/node.rs index 92d3991..fbd99d2 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use crate::{ - state::{DynamicState, EndWildcardState, NodeState, StaticState, WildcardState}, + state::{ + DynamicConstrainedState, DynamicState, EndWildcardConstrainedState, EndWildcardState, + NodeState, StaticState, WildcardConstrainedState, WildcardState, + }, vec::SortedNode, }; @@ -58,10 +61,13 @@ 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. diff --git a/src/optimize.rs b/src/optimize.rs index f02ba71..16df2b6 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -19,21 +19,36 @@ impl Node<'_, T, S> { child.optimize_inner(self.priority); } + for child in self.dynamic_constrained_children.iter_mut() { + child.optimize_inner(self.priority); + } + for child in self.dynamic_children.iter_mut() { child.optimize_inner(self.priority); } + for child in self.wildcard_constrained_children.iter_mut() { + child.optimize_inner(self.priority); + } + for child in self.wildcard_children.iter_mut() { child.optimize_inner(self.priority); } + for child in self.end_wildcard_constrained_children.iter_mut() { + child.optimize_inner(self.priority); + } + for child in self.end_wildcard_children.iter_mut() { child.optimize_inner(self.priority); } 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,7 +57,6 @@ 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() { @@ -54,7 +68,37 @@ impl Node<'_, T, S> { } fn update_dynamic_children_shortcut(&mut self) { - self.dynamic_children_shortcut = self.dynamic_children.iter().all(|child| { + let constrained_check = self.dynamic_constrained_children.iter().all(|child| { + // Leading slash? + if child.state.name.as_bytes().first() == Some(&b'/') { + return true; + } + + // 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.dynamic_children.iter().all(|child| { // Leading slash? if child.state.name.as_bytes().first() == Some(&b'/') { return true; @@ -62,8 +106,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 +127,19 @@ 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; @@ -104,5 +156,32 @@ impl Node<'_, T, S> { 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; + } + + // All static children start with a slash? + if child + .static_children + .iter() + .all(|child| child.state.prefix.first() == Some(&b'/')) + { + return true; + } + + false + }); + + self.wildcard_children_shortcut = constrained_check && unconstrained_check; } } diff --git a/src/parser.rs b/src/parser.rs index e64117c..26553bc 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 623e024..1c943b7 100644 --- a/src/router.rs +++ b/src/router.rs @@ -9,7 +9,7 @@ use smallvec::{smallvec, SmallVec}; use crate::{ constraints::Constraint, - errors::{ConstraintError, DeleteError, InsertError, SearchError}, + errors::{ConstraintError, DeleteError, InsertError}, node::{Node, NodeData}, parser::{ParsedTemplate, Part}, state::RootState, @@ -47,10 +47,13 @@ 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, @@ -106,13 +109,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()) { @@ -203,15 +204,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, _ => { - return Ok(None); + return None; } }; @@ -225,12 +226,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 24cb369..8c4fbba 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>, usize)> { if path.is_empty() { - return Ok(self.data.as_ref().map(|data| (data, self.priority))); + return self.data.as_ref().map(|data| (data, self.priority)); } - 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,34 +83,76 @@ 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>, usize)> { 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)? + child.search(remaining_path, parameters, constraints) { - return Ok(Some((data, priority))); + return Some((data, priority)); } } } - Ok(None) + None } - fn search_dynamic<'p>( + /// Can handle complex dynamic routes 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> { - 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>, usize)> { + 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_parameters = smallvec![]; + + while consumed < path.len() { + if path[consumed] == b'/' { + break; + } + + consumed += 1; + + let segment = &path[..consumed]; + 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).ok()?)); + + let (data, priority) = + match child.search(&path[consumed..], &mut current_parameters, constraints) { + Some((data, priority)) => (data, priority), + _ => { + continue; + } + }; + + if best_match.is_none() + || best_match + .as_ref() + .map_or(false, |(_, best_priority)| priority >= *best_priority) + { + best_match = Some((data, priority)); + best_match_parameters = current_parameters; + } + } + + if let Some(result) = best_match { + *parameters = best_match_parameters; + return Some(result); + } } + + None } /// Can handle complex dynamic routes like `{name}.{extension}`. @@ -84,7 +161,7 @@ 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>, usize)> { for child in self.dynamic_children.iter() { let mut consumed = 0; @@ -99,20 +176,12 @@ 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) { - 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(), - })?, - )); + current_parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); let (data, priority) = - match child.search(&path[consumed..], &mut current_parameters, constraints)? { + match child.search(&path[consumed..], &mut current_parameters, constraints) { Some((data, priority)) => (data, priority), _ => { continue; @@ -131,66 +200,72 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { 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}/`. - fn search_dynamic_segment<'p>( + fn search_dynamic_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.dynamic_children.iter() { + ) -> Option<(&'r NodeData<'r, T>, usize)> { + 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(child.state.constraint.as_ref(), segment, constraints) { + if !Self::check_constraint(Some(&child.state.constraint), 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 only handle simple dynamic routes 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> { - 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>, usize)> { + 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]; + + 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 wildcard routes like `/{*name}.{extension}`. - fn search_wildcard_inline<'p>( + fn search_wildcard_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.wildcard_children.iter() { + ) -> Option<(&'r NodeData<'r, T>, usize)> { + for child in self.wildcard_constrained_children.iter() { let mut consumed = 0; let mut best_match: Option<(&'r NodeData<'r, T>, usize)> = None; @@ -200,20 +275,15 @@ 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(), - })?, - )); + current_parameters.push((&child.state.name, std::str::from_utf8(segment).ok()?)); let (data, priority) = - match child.search(&path[consumed..], &mut current_parameters, constraints)? { + match child.search(&path[consumed..], &mut current_parameters, constraints) { Some((data, priority)) => (data, priority), _ => { continue; @@ -232,22 +302,70 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { 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}/`. - fn search_wildcard_segment<'p>( + /// Can handle complex wildcard routes like `/{*name}.{extension}`. + fn search_wildcard_inline<'p>( &'r self, path: &'p [u8], parameters: &mut Parameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, SearchError> { + ) -> Option<(&'r NodeData<'r, T>, usize)> { for child in self.wildcard_children.iter() { let mut consumed = 0; + + let mut best_match: Option<(&'r NodeData<'r, T>, usize)> = 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, priority) = + match child.search(&path[consumed..], &mut current_parameters, constraints) { + Some((data, priority)) => (data, priority), + _ => { + continue; + } + }; + + if best_match.is_none() + || best_match + .as_ref() + .map_or(false, |(_, best_priority)| priority >= *best_priority) + { + best_match = Some((data, priority)); + best_match_parameters = current_parameters; + } + } + + if let Some(result) = best_match { + *parameters = best_match_parameters; + return Some(result); + } + } + + None + } + + /// Can only handle simple wildcard routes like `/{*segment}/`. + fn search_wildcard_constrained_segment<'p>( + &'r self, + path: &'p [u8], + parameters: &mut Parameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Option<(&'r NodeData<'r, T>, usize)> { + for child in self.wildcard_constrained_children.iter() { + let mut consumed = 0; let mut remaining_path = path; let mut section_end = false; @@ -275,21 +393,16 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { &path[..consumed] }; - if !Self::check_constraint(child.state.constraint.as_ref(), segment, constraints) { + if !Self::check_constraint(Some(&child.state.constraint), 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 +415,95 @@ impl<'r, T, S: NodeState> Node<'r, T, S> { } } - Ok(None) + None } - fn search_end_wildcard<'p>( + /// Can only handle simple wildcard routes like `/{*segment}/`. + fn search_wildcard_segment<'p>( + &'r self, + path: &'p [u8], + parameters: &mut Parameters<'r, 'p>, + constraints: &HashMap<&'r str, StoredConstraint>, + ) -> Option<(&'r NodeData<'r, T>, usize)> { + for child in self.wildcard_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] + }; + + 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 + } + + 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>, usize)> { + 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().map(|data| (data, child.priority)); + } + + 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>, usize)> { + 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().map(|data| (data, child.priority)); } - Ok(None) + None } fn check_constraint( diff --git a/src/state.rs b/src/state.rs index ae4edd4..52fddf3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,5 @@ 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; @@ -105,7 +103,6 @@ impl PartialOrd for StaticState { #[derive(Clone, Debug, Eq, PartialEq)] pub struct DynamicState { pub name: String, - pub constraint: Option, priority: usize, padding: usize, key: String, @@ -113,17 +110,61 @@ pub struct DynamicState { 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 priority = name.len(); + let padding = name.len().saturating_sub(1); + let key = format!("{{{name}}}"); + + Self { + name, + priority, + padding, + key, } + } +} + +impl NodeState for DynamicState { + fn priority(&self) -> usize { + self.priority + } + + 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, + priority: usize, + padding: usize, + key: String, +} + +impl DynamicConstrainedState { + #[must_use] + pub fn new(name: String, constraint: String) -> Self { + let priority = name.len() + 10_000; 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, @@ -135,7 +176,7 @@ impl DynamicState { } } -impl NodeState for DynamicState { +impl NodeState for DynamicConstrainedState { fn priority(&self) -> usize { self.priority } @@ -149,7 +190,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 +198,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,7 +207,6 @@ impl PartialOrd for DynamicState { #[derive(Clone, Debug, Eq, PartialEq)] pub struct WildcardState { pub name: String, - pub constraint: Option, priority: usize, padding: usize, key: String, @@ -174,17 +214,61 @@ pub struct WildcardState { 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 priority = name.len(); + let padding = name.len().saturating_sub(1); + let key = format!("{{*{name}}}"); + + Self { + name, + priority, + padding, + key, } + } +} + +impl NodeState for WildcardState { + fn priority(&self) -> usize { + self.priority + } + + 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, + priority: usize, + padding: usize, + key: String, +} +impl WildcardConstrainedState { + #[must_use] + pub fn new(name: String, constraint: String) -> Self { + let priority = name.len() + 10_000; 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, @@ -196,7 +280,7 @@ impl WildcardState { } } -impl NodeState for WildcardState { +impl NodeState for WildcardConstrainedState { fn priority(&self) -> usize { self.priority } @@ -210,7 +294,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 +302,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,7 +311,6 @@ impl PartialOrd for WildcardState { #[derive(Clone, Debug, Eq, PartialEq)] pub struct EndWildcardState { pub name: String, - pub constraint: Option, priority: usize, padding: usize, key: String, @@ -235,17 +318,61 @@ pub struct EndWildcardState { 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 priority = name.len(); + let padding = name.len().saturating_sub(1); + let key = format!("{{*{name}}}"); + + Self { + name, + priority, + padding, + key, } + } +} + +impl NodeState for EndWildcardState { + fn priority(&self) -> usize { + self.priority + } + + 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, + priority: usize, + padding: usize, + key: String, +} + +impl EndWildcardConstrainedState { + #[must_use] + pub fn new(name: String, constraint: String) -> Self { + let priority = name.len() + 10_000; 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, @@ -257,7 +384,7 @@ impl EndWildcardState { } } -impl NodeState for EndWildcardState { +impl NodeState for EndWildcardConstrainedState { fn priority(&self) -> usize { self.priority } @@ -271,7 +398,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 +406,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 728d814..076e14a 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/dynamic.rs b/tests/dynamic.rs index 5e01af5..5ec72c7 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 31e48dc..33ae0df 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/optional.rs b/tests/optional.rs index 32f3ebf..a68633a 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 e7e1d6b..156959b 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(()) @@ -138,7 +138,7 @@ fn test_static_split_multibyte() -> Result<(), Box> { ╰─ � [*] "); - 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 ef1e09a..92492b1 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 { @@ -218,7 +218,7 @@ fn test_wildcard_priority() -> Result<(), Box> { ╰─ /static [*] "); - 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 {