diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a5f3faf..ad4bf2fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Optional parameters are now supported in routes. + ### Changed - Successful matches now return a flattened representation of node data. diff --git a/README.md b/README.md index 08976245..5011d757 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,55 @@ fn main() -> Result<(), Box> { } ``` +### Optional Parameters + +`wayfind` supports optional parameters for both dynamic and wildcard routes. + +TODO + +#### Example + +```rust +use std::error::Error; +use wayfind::{Path, Router}; + +fn main() -> Result<(), Box> { + let mut router = Router::new(); + router.insert("/users/{id?}", 1)?; + router.insert("/files/{*files}/{file}.{extension?}", 2)?; + + let path = Path::new("/users")?; + let search = router.search(&path)?.unwrap(); + assert_eq!(*search.data, 1); + + let path = Path::new("/users/123")?; + let search = router.search(&path)?.unwrap(); + assert_eq!(*search.data, 1); + assert_eq!(search.parameters[0].key, "id"); + assert_eq!(search.parameters[0].value, "123"); + + let path = Path::new("/files/documents/folder/report.pdf")?; + let search = router.search(&path)?.unwrap(); + assert_eq!(*search.data, 2); + assert_eq!(search.parameters[0].key, "files"); + assert_eq!(search.parameters[0].value, "documents/folder"); + assert_eq!(search.parameters[1].key, "file"); + assert_eq!(search.parameters[1].value, "report"); + assert_eq!(search.parameters[2].key, "extension"); + assert_eq!(search.parameters[2].value, "pdf"); + + let path = Path::new("/files/documents/folder/readme")?; + let search = router.search(&path)?.unwrap(); + assert_eq!(*search.data, 2); + assert_eq!(search.parameters[0].key, "files"); + assert_eq!(search.parameters[0].value, "documents/folder"); + assert_eq!(search.parameters[1].key, "file"); + assert_eq!(search.parameters[1].value, "readme"); + + Ok(()) +} +``` + ### Constraints Constraints allow for custom logic to be injected into the routing process. diff --git a/src/expander.rs b/src/expander.rs new file mode 100644 index 00000000..ebcfe74d --- /dev/null +++ b/src/expander.rs @@ -0,0 +1,246 @@ +use crate::parser::{ParsedRoute, RoutePart, RouteParts}; +use std::collections::VecDeque; + +/// Represents a collection of expanded, simplified routes, derived from an original route. +#[derive(Debug, PartialEq, Eq)] +pub struct ExpandedRoutes { + pub raw: ParsedRoute, + pub routes: Vec, +} + +impl ExpandedRoutes { + pub fn new(route: ParsedRoute) -> Self { + let mut routes = VecDeque::new(); + Self::recursive_expand(route.parts.0.clone(), VecDeque::new(), &mut routes); + + let routes = + routes + .into_iter() + .map(|parts| { + // Handle special case, where optional is at the start of a route. + // Replace with a single "/" part. + if parts.0.iter().all( + |part| matches!(part, RoutePart::Static { prefix } if prefix.is_empty()), + ) { + RouteParts(VecDeque::from(vec![RoutePart::Static { + prefix: b"/".to_vec(), + }])) + } else { + parts + } + }) + .map(ParsedRoute::from) + .collect(); + + Self { raw: route, routes } + } + + fn recursive_expand( + mut remaining: VecDeque, + mut current: VecDeque, + expanded: &mut VecDeque, + ) { + let Some(part) = remaining.pop_front() else { + expanded.push_back(RouteParts(current)); + return; + }; + + if !part.is_optional() { + current.push_back(part); + Self::recursive_expand(remaining, current, expanded); + return; + } + + let mut new_part = current.clone(); + new_part.push_back(part.clone().disable_optional()); + Self::recursive_expand(remaining.clone(), new_part, expanded); + + if part.is_segment(current.back(), remaining.front()) { + if let Some(RoutePart::Static { prefix }) = current.back_mut() { + prefix.pop(); + } + + Self::recursive_expand(remaining, current, expanded); + } else { + let mut trimmed_current = current.clone(); + while let Some(last) = trimmed_current.back() { + if matches!(last, RoutePart::Static { prefix } if !prefix.is_empty() && !last.ends_with_slash()) + { + trimmed_current.pop_back(); + } else { + break; + } + } + + expanded.push_back(RouteParts(trimmed_current)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use similar_asserts::assert_eq; + use std::error::Error; + + #[test] + fn test_expander_segment_simple() -> Result<(), Box> { + let route = ParsedRoute::new(b"/users/{id?}")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/users/{id}")?, + ParsedRoute::new(b"/users")? + ], + } + ); + + Ok(()) + } + + #[test] + fn test_expander_segment_simple_suffix() -> Result<(), Box> { + let route = ParsedRoute::new(b"/users/{id?}/info")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/users/{id}/info")?, + ParsedRoute::new(b"/users/info")? + ], + } + ); + + Ok(()) + } + + #[test] + fn test_expander_segment_multiple() -> Result<(), Box> { + let route = ParsedRoute::new(b"/users/{id?}/{name?}")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/users/{id}/{name}")?, + ParsedRoute::new(b"/users/{id}")?, + ParsedRoute::new(b"/users/{name}")?, + ParsedRoute::new(b"/users")?, + ] + } + ); + + Ok(()) + } + + #[test] + fn test_expander_segment_constraint() -> Result<(), Box> { + let route = ParsedRoute::new(b"/users/{id?:int}")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/users/{id:int}")?, + ParsedRoute::new(b"/users")?, + ] + } + ); + + Ok(()) + } + + #[test] + fn test_expander_segment_all_optional() -> Result<(), Box> { + let route = ParsedRoute::new(b"/{category?}/{subcategory?}/{id?}")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/{category}/{subcategory}/{id}")?, + ParsedRoute::new(b"/{category}/{subcategory}")?, + ParsedRoute::new(b"/{category}/{id}")?, + ParsedRoute::new(b"/{category}")?, + ParsedRoute::new(b"/{subcategory}/{id}")?, + ParsedRoute::new(b"/{subcategory}")?, + ParsedRoute::new(b"/{id}")?, + ParsedRoute::new(b"/")?, + ] + } + ); + + Ok(()) + } + + #[test] + fn test_expander_segment_mixed() -> Result<(), Box> { + let route = ParsedRoute::new(b"/api/{version}/{resource}/{id?}/details")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/api/{version}/{resource}/{id}/details")?, + ParsedRoute::new(b"/api/{version}/{resource}/details")?, + ] + } + ); + + Ok(()) + } + + #[test] + fn test_expander_inline_simple() -> Result<(), Box> { + let route = ParsedRoute::new(b"/files/{name}.{extension?}")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/files/{name}.{extension}")?, + ParsedRoute::new(b"/files/{name}")?, + ], + } + ); + + Ok(()) + } + + #[test] + fn test_expander_inline_multiple() -> Result<(), Box> { + let route = ParsedRoute::new(b"/release/v{major}.{minor?}.{patch?}")?; + let expanded = ExpandedRoutes::new(route.clone()); + + assert_eq!( + expanded, + ExpandedRoutes { + raw: route, + routes: vec![ + ParsedRoute::new(b"/release/v{major}.{minor}.{patch}")?, + ParsedRoute::new(b"/release/v{major}.{minor}")?, + ParsedRoute::new(b"/release/v{major}")?, + ], + } + ); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index a782db62..618385cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ pub(crate) mod decode; pub mod errors; +pub(crate) mod expander; + pub(crate) mod node; pub use node::search::{Match, Parameter}; diff --git a/src/node.rs b/src/node.rs index c740f805..e48e9027 100644 --- a/src/node.rs +++ b/src/node.rs @@ -30,7 +30,7 @@ pub enum NodeKind { pub enum NodeData { /// Data is stored inline. Inline { - /// The original route path. + /// The original route. route: Arc, /// The associated data. @@ -38,8 +38,13 @@ pub enum NodeData { }, /// Data is stored at the router level, as it's shared between 2 or more nodes. - #[allow(dead_code)] - Reference(Arc), + Reference { + /// The original route. + route: Arc, + + /// The expanded route. + expanded: Arc, + }, } /// Represents a node in the tree structure. diff --git a/src/node/delete.rs b/src/node/delete.rs index 7e50e513..876d467a 100644 --- a/src/node/delete.rs +++ b/src/node/delete.rs @@ -11,19 +11,19 @@ impl Node { /// Logic should match that used by the insert method. /// /// If the route is found and deleted, we re-optimize the tree structure. - pub fn delete(&mut self, route: &mut ParsedRoute<'_>) -> Result<(), DeleteError> { + pub fn delete(&mut self, route: &mut ParsedRoute) -> Result<(), DeleteError> { if let Some(part) = route.parts.pop_front() { let result = match part { RoutePart::Static { prefix } => self.delete_static(route, &prefix), - RoutePart::Dynamic { name, constraint } => { - self.delete_dynamic(route, &name, &constraint) - } - RoutePart::Wildcard { name, constraint } if route.parts.is_empty() => { - self.delete_end_wildcard(route, &name, &constraint) - } - RoutePart::Wildcard { name, constraint } => { - self.delete_wildcard(route, &name, &constraint) - } + RoutePart::Dynamic { + name, constraint, .. + } => self.delete_dynamic(route, &name, &constraint), + RoutePart::Wildcard { + name, constraint, .. + } if route.parts.is_empty() => self.delete_end_wildcard(route, &name, &constraint), + RoutePart::Wildcard { + name, constraint, .. + } => self.delete_wildcard(route, &name, &constraint), }; if result.is_ok() { @@ -38,16 +38,12 @@ impl Node { } Err(DeleteError::NotFound { - route: String::from_utf8_lossy(route.raw).to_string(), + route: String::from_utf8_lossy(&route.raw).to_string(), }) } } - fn delete_static( - &mut self, - route: &mut ParsedRoute<'_>, - prefix: &[u8], - ) -> Result<(), DeleteError> { + fn delete_static(&mut self, route: &mut ParsedRoute, prefix: &[u8]) -> Result<(), DeleteError> { let index = self .static_children .iter() @@ -56,7 +52,7 @@ impl Node { && child.prefix.iter().zip(prefix).all(|(a, b)| a == b) }) .ok_or(DeleteError::NotFound { - route: String::from_utf8_lossy(route.raw).to_string(), + route: String::from_utf8_lossy(&route.raw).to_string(), })?; let child = &mut self.static_children[index]; @@ -81,7 +77,7 @@ impl Node { fn delete_dynamic( &mut self, - route: &mut ParsedRoute<'_>, + route: &mut ParsedRoute, name: &[u8], constraint: &Option>, ) -> Result<(), DeleteError> { @@ -90,7 +86,7 @@ impl Node { .iter() .position(|child| child.prefix == name && child.constraint == *constraint) .ok_or(DeleteError::NotFound { - route: String::from_utf8_lossy(route.raw).to_string(), + route: String::from_utf8_lossy(&route.raw).to_string(), })?; let child = &mut self.dynamic_children[index]; @@ -109,7 +105,7 @@ impl Node { fn delete_wildcard( &mut self, - route: &mut ParsedRoute<'_>, + route: &mut ParsedRoute, name: &[u8], constraint: &Option>, ) -> Result<(), DeleteError> { @@ -118,7 +114,7 @@ impl Node { .iter() .position(|child| child.prefix == name && child.constraint == *constraint) .ok_or(DeleteError::NotFound { - route: String::from_utf8_lossy(route.raw).to_string(), + route: String::from_utf8_lossy(&route.raw).to_string(), })?; let child = &mut self.wildcard_children[index]; @@ -137,7 +133,7 @@ impl Node { fn delete_end_wildcard( &mut self, - route: &ParsedRoute<'_>, + route: &ParsedRoute, name: &[u8], constraint: &Option>, ) -> Result<(), DeleteError> { @@ -146,7 +142,7 @@ impl Node { .iter() .position(|child| child.prefix == name && child.constraint == *constraint) .ok_or(DeleteError::NotFound { - route: String::from_utf8_lossy(route.raw).to_string(), + route: String::from_utf8_lossy(&route.raw).to_string(), })?; self.end_wildcard_children.remove(index); diff --git a/src/node/insert.rs b/src/node/insert.rs index c6e5114a..2eba1a8e 100644 --- a/src/node/insert.rs +++ b/src/node/insert.rs @@ -12,26 +12,32 @@ impl Node { /// Will error is there's already data at the end node. pub fn insert( &mut self, - route: &mut ParsedRoute<'_>, + route: &mut ParsedRoute, data: NodeData, ) -> Result<(), InsertError> { if let Some(part) = route.parts.pop_front() { match part { RoutePart::Static { prefix } => self.insert_static(route, data, &prefix)?, - RoutePart::Dynamic { name, constraint } => { + RoutePart::Dynamic { + name, constraint, .. + } => { self.insert_dynamic(route, data, &name, constraint)?; } - RoutePart::Wildcard { name, constraint } if route.parts.is_empty() => { + RoutePart::Wildcard { + name, constraint, .. + } if route.parts.is_empty() => { self.insert_end_wildcard(route, data, &name, constraint)?; } - RoutePart::Wildcard { name, constraint } => { + RoutePart::Wildcard { + name, constraint, .. + } => { self.insert_wildcard(route, data, &name, constraint)?; } }; } else { if self.data.is_some() { return Err(InsertError::DuplicateRoute { - route: String::from_utf8_lossy(route.raw).to_string(), + route: String::from_utf8_lossy(&route.raw).to_string(), }); } @@ -46,7 +52,7 @@ impl Node { fn insert_static( &mut self, - route: &mut ParsedRoute<'_>, + route: &mut ParsedRoute, data: NodeData, prefix: &[u8], ) -> Result<(), InsertError> { @@ -142,7 +148,7 @@ impl Node { fn insert_dynamic( &mut self, - route: &mut ParsedRoute<'_>, + route: &mut ParsedRoute, data: NodeData, name: &[u8], constraint: Option>, @@ -180,7 +186,7 @@ impl Node { fn insert_wildcard( &mut self, - route: &mut ParsedRoute<'_>, + route: &mut ParsedRoute, data: NodeData, name: &[u8], constraint: Option>, @@ -218,7 +224,7 @@ impl Node { fn insert_end_wildcard( &mut self, - route: &ParsedRoute<'_>, + route: &ParsedRoute, data: NodeData, name: &[u8], constraint: Option>, @@ -229,7 +235,7 @@ impl Node { .any(|child| child.prefix == name && child.constraint == constraint) { return Err(InsertError::DuplicateRoute { - route: String::from_utf8_lossy(route.raw).to_string(), + route: String::from_utf8_lossy(&route.raw).to_string(), }); } diff --git a/src/node/search.rs b/src/node/search.rs index 80fd955b..9a1e4fdd 100644 --- a/src/node/search.rs +++ b/src/node/search.rs @@ -175,7 +175,8 @@ impl Node { }; match data { - NodeData::Inline { route, .. } | NodeData::Reference(route) => route.len(), + NodeData::Inline { route, .. } => route.len(), + NodeData::Reference { expanded, .. } => expanded.len(), } } diff --git a/src/parser.rs b/src/parser.rs index 7f66b0f2..ba47d035 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,11 +1,14 @@ use crate::errors::RouteError; -use std::{collections::VecDeque, fmt::Debug}; +use std::{ + collections::VecDeque, + fmt::{self, Debug}, +}; /// Characters that are not allowed in parameter names or constraints. const INVALID_PARAM_CHARS: [u8; 6] = [b':', b'*', b'?', b'{', b'}', b'/']; /// A parsed section of a route. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RoutePart { Static { prefix: Vec, @@ -13,18 +16,64 @@ pub enum RoutePart { Dynamic { name: Vec, + optional: bool, constraint: Option>, }, Wildcard { name: Vec, + optional: bool, constraint: Option>, }, } +impl RoutePart { + pub const fn is_optional(&self) -> bool { + matches!( + self, + Self::Dynamic { optional: true, .. } | Self::Wildcard { optional: true, .. } + ) + } + + pub fn disable_optional(mut self) -> Self { + match &mut self { + Self::Dynamic { optional, .. } | Self::Wildcard { optional, .. } => { + *optional = false; + } + Self::Static { .. } => {} + } + + self + } + + pub fn is_segment(&self, prev: Option<&Self>, next: Option<&Self>) -> bool { + match self { + Self::Static { .. } => false, + _ => { + prev.map_or(true, Self::ends_with_slash) + || next.map_or(true, Self::starts_with_slash) + } + } + } + + pub fn ends_with_slash(&self) -> bool { + match self { + Self::Static { prefix } => prefix.last() == Some(&b'/'), + _ => false, + } + } + + pub fn starts_with_slash(&self) -> bool { + match self { + Self::Static { prefix } => prefix.first() == Some(&b'/'), + _ => false, + } + } +} + /// The parsed parts of the route, in order. /// We may want these to simply be indicies of the original route in the future, to reduce allocations. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct RouteParts(pub VecDeque); impl RouteParts { @@ -42,6 +91,55 @@ impl RouteParts { } } +impl fmt::Display for RouteParts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for part in &self.0 { + match part { + RoutePart::Static { prefix } => { + write!(f, "{}", String::from_utf8_lossy(prefix))?; + } + RoutePart::Dynamic { + name, + optional, + constraint, + .. + } => { + write!(f, "{{{}", String::from_utf8_lossy(name))?; + if *optional { + write!(f, "?")?; + } + + if let Some(constraint) = constraint { + write!(f, ":{}", String::from_utf8_lossy(constraint))?; + } + + write!(f, "}}")?; + } + RoutePart::Wildcard { + name, + optional, + constraint, + .. + } => { + write!(f, "{{*{}", String::from_utf8_lossy(name))?; + + if *optional { + write!(f, "?")?; + } + + if let Some(constraint) = constraint { + write!(f, ":{}", String::from_utf8_lossy(constraint))?; + } + + write!(f, "}}")?; + } + } + } + + Ok(()) + } +} + impl IntoIterator for RouteParts { type Item = RoutePart; type IntoIter = std::collections::vec_deque::IntoIter; @@ -61,14 +159,14 @@ impl<'a> IntoIterator for &'a RouteParts { } /// A parsed route. -#[derive(Debug, PartialEq, Eq)] -pub struct ParsedRoute<'a> { - pub raw: &'a [u8], +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedRoute { + pub raw: Vec, pub parts: RouteParts, } -impl<'a> ParsedRoute<'a> { - pub fn new(route: &'a [u8]) -> Result { +impl ParsedRoute { + pub fn new(route: &[u8]) -> Result { if route.is_empty() { return Err(RouteError::EmptyRoute); } @@ -116,7 +214,7 @@ impl<'a> ParsedRoute<'a> { } Ok(Self { - raw: route, + raw: route.to_vec(), parts: RouteParts(parts), }) } @@ -160,6 +258,12 @@ impl<'a> ParsedRoute<'a> { (false, name) }; + let (optional, name) = if name.ends_with(b"?") { + (true, &name[..name.len() - 1]) + } else { + (false, name) + }; + if name.is_empty() { if wildcard { return Err(RouteError::EmptyWildcard { @@ -207,11 +311,13 @@ impl<'a> ParsedRoute<'a> { let part = if wildcard { RoutePart::Wildcard { name: name.to_vec(), + optional, constraint: constraint.map(<[u8]>::to_vec), } } else { RoutePart::Dynamic { name: name.to_vec(), + optional, constraint: constraint.map(<[u8]>::to_vec), } }; @@ -221,6 +327,37 @@ impl<'a> ParsedRoute<'a> { } } +impl From for ParsedRoute { + fn from(value: RouteParts) -> Self { + let mut parts = VecDeque::new(); + let mut current_static = Vec::new(); + + for part in value.0 { + if let RoutePart::Static { prefix } = part { + current_static.extend_from_slice(&prefix); + } else { + if !current_static.is_empty() { + parts.push_back(RoutePart::Static { + prefix: std::mem::take(&mut current_static), + }); + } + parts.push_back(part); + } + } + + if !current_static.is_empty() { + parts.push_back(RoutePart::Static { + prefix: current_static, + }); + } + + let parts = RouteParts(parts); + let raw = parts.to_string().into_bytes(); + + Self { raw, parts } + } +} + #[cfg(test)] mod tests { use super::*; @@ -231,7 +368,7 @@ mod tests { assert_eq!( ParsedRoute::new(b"/abcd"), Ok(ParsedRoute { - raw: b"/abcd", + raw: b"/abcd".to_vec(), parts: RouteParts(VecDeque::from(vec![RoutePart::Static { prefix: b"/abcd".to_vec() }])) @@ -244,13 +381,14 @@ mod tests { assert_eq!( ParsedRoute::new(b"/{name}"), Ok(ParsedRoute { - raw: b"/{name}", + raw: b"/{name}".to_vec(), parts: RouteParts(VecDeque::from(vec![ RoutePart::Static { prefix: b"/".to_vec() }, RoutePart::Dynamic { name: b"name".to_vec(), + optional: false, constraint: None }, ])) @@ -263,13 +401,50 @@ mod tests { assert_eq!( ParsedRoute::new(b"/{*route}"), Ok(ParsedRoute { - raw: b"/{*route}", + raw: b"/{*route}".to_vec(), parts: RouteParts(VecDeque::from(vec![ RoutePart::Static { prefix: b"/".to_vec() }, RoutePart::Wildcard { name: b"route".to_vec(), + optional: false, + constraint: None + }, + ])) + }) + ); + } + + #[test] + fn test_parts_optional() { + assert_eq!( + ParsedRoute::new(b"/release/v{major}.{minor?}.{patch?}"), + Ok(ParsedRoute { + raw: b"/release/v{major}.{minor?}.{patch?}".to_vec(), + parts: RouteParts(VecDeque::from(vec![ + RoutePart::Static { + prefix: b"/release/v".to_vec() + }, + RoutePart::Dynamic { + name: b"major".to_vec(), + optional: false, + constraint: None + }, + RoutePart::Static { + prefix: b".".to_vec() + }, + RoutePart::Dynamic { + name: b"minor".to_vec(), + optional: true, + constraint: None + }, + RoutePart::Static { + prefix: b".".to_vec() + }, + RoutePart::Dynamic { + name: b"patch".to_vec(), + optional: true, constraint: None }, ])) @@ -282,13 +457,14 @@ mod tests { assert_eq!( ParsedRoute::new(b"/{name:alpha}/{id:numeric}"), Ok(ParsedRoute { - raw: b"/{name:alpha}/{id:numeric}", + raw: b"/{name:alpha}/{id:numeric}".to_vec(), parts: RouteParts(VecDeque::from(vec![ RoutePart::Static { prefix: b"/".to_vec() }, RoutePart::Dynamic { name: b"name".to_vec(), + optional: false, constraint: Some(b"alpha".to_vec()) }, RoutePart::Static { @@ -296,6 +472,7 @@ mod tests { }, RoutePart::Dynamic { name: b"id".to_vec(), + optional: false, constraint: Some(b"numeric".to_vec()) }, ])) @@ -450,7 +627,7 @@ mod tests { assert_eq!( ParsedRoute::new(b"/{{name}}"), Ok(ParsedRoute { - raw: b"/{{name}}", + raw: b"/{{name}}".to_vec(), parts: RouteParts(VecDeque::from(vec![RoutePart::Static { prefix: b"/{name}".to_vec() }])) @@ -460,7 +637,7 @@ mod tests { assert_eq!( ParsedRoute::new(b"/name}}"), Ok(ParsedRoute { - raw: b"/name}}", + raw: b"/name}}".to_vec(), parts: RouteParts(VecDeque::from(vec![RoutePart::Static { prefix: b"/name}".to_vec() }])) @@ -470,7 +647,7 @@ mod tests { assert_eq!( ParsedRoute::new(b"/name{{"), Ok(ParsedRoute { - raw: b"/name{{", + raw: b"/name{{".to_vec(), parts: RouteParts(VecDeque::from(vec![RoutePart::Static { prefix: b"/name{".to_vec() }])) @@ -480,13 +657,14 @@ mod tests { assert_eq!( ParsedRoute::new(b"/{{{name}}}"), Ok(ParsedRoute { - raw: b"/{{{name}}}", + raw: b"/{{{name}}}".to_vec(), parts: RouteParts(VecDeque::from(vec![ RoutePart::Static { prefix: b"/{".to_vec() }, RoutePart::Dynamic { name: b"name".to_vec(), + optional: false, constraint: None }, RoutePart::Static { @@ -499,7 +677,7 @@ mod tests { assert_eq!( ParsedRoute::new(b"/{{{{name}}}}"), Ok(ParsedRoute { - raw: b"/{{{{name}}}}", + raw: b"/{{{{name}}}}".to_vec(), parts: RouteParts(VecDeque::from(vec![RoutePart::Static { prefix: b"/{{name}}".to_vec() }])) @@ -509,7 +687,7 @@ mod tests { assert_eq!( ParsedRoute::new(b"{{}}"), Ok(ParsedRoute { - raw: b"{{}}", + raw: b"{{}}".to_vec(), parts: RouteParts(VecDeque::from(vec![RoutePart::Static { prefix: b"{}".to_vec() }])) @@ -519,7 +697,7 @@ mod tests { assert_eq!( ParsedRoute::new(b"{{:}}"), Ok(ParsedRoute { - raw: b"{{:}}", + raw: b"{{:}}".to_vec(), parts: RouteParts(VecDeque::from(vec![RoutePart::Static { prefix: b"{:}".to_vec() }])) diff --git a/src/router.rs b/src/router.rs index bb7e3952..e37e1422 100644 --- a/src/router.rs +++ b/src/router.rs @@ -2,6 +2,7 @@ use crate::{ constraints::Constraint, decode::percent_decode, errors::{ConstraintError, DeleteError, EncodingError, InsertError, SearchError}, + expander::ExpandedRoutes, node::{search::Match, Node, NodeData, NodeKind}, parser::{ParsedRoute, RoutePart}, path::Path, @@ -173,13 +174,32 @@ impl Router { } } - // TODO: Using Parts, determine whether we need to store inline or not. - let node_data = NodeData::Inline { - route: Arc::clone(&route_arc), - value, + let expanded = ExpandedRoutes::new(route.clone()); + if expanded.routes.len() > 1 { + self.data.insert(Arc::clone(&route_arc), value); + + for mut route in expanded.routes { + let expanded = Arc::from(route.parts.to_string()); + + self.root.insert( + &mut route, + NodeData::Reference { + route: Arc::clone(&route_arc), + expanded, + }, + )?; + } + } else { + self.root.insert( + &mut route, + NodeData::Inline { + route: Arc::clone(&route_arc), + value, + }, + )?; }; - self.root.insert(&mut route, node_data) + Ok(()) } /// Deletes a route from the router. @@ -188,11 +208,11 @@ impl Router { /// /// # Errors /// - /// Returns a [`DeleteError`] if the route cannot be deleted, or cannot be found. + /// Returns a [`DeleteError`] if the route is invalid, cannot be deleted, or cannot be found. /// /// # Examples /// - /// ```rust + /// ```rustg /// use wayfind::{Constraint, Router}; /// /// let mut router: Router = Router::new(); @@ -208,8 +228,22 @@ impl Router { })?; } + let route_arc = Arc::from(route); + let mut route = ParsedRoute::new(route.as_bytes())?; - self.root.delete(&mut route) + let expanded = ExpandedRoutes::new(route.clone()); + + if expanded.routes.len() > 1 { + self.data.remove(&route_arc); + + for mut expanded_route in expanded.routes { + self.root.delete(&mut expanded_route)?; + } + } else { + self.root.delete(&mut route)?; + } + + Ok(()) } /// Searches for a matching route in the router. @@ -245,12 +279,12 @@ impl Router { let (route, data) = match &node.data { Some(NodeData::Inline { route, value }) => (Arc::clone(route), value), - Some(NodeData::Reference(key)) => { - let Some(data) = self.data.get(key) else { + Some(NodeData::Reference { route, .. }) => { + let Some(data) = self.data.get(route) else { return Ok(None); }; - (Arc::clone(key), data) + (Arc::clone(route), data) } None => return Ok(None), }; diff --git a/tests/edge_cases.rs b/tests/edge_cases.rs index 1e0a02b1..194020cd 100644 --- a/tests/edge_cases.rs +++ b/tests/edge_cases.rs @@ -5,7 +5,7 @@ use wayfind::Router; mod utils; #[test] -fn test_depth_matching_simple() -> Result<(), Box> { +fn test_specific_matching_simple() -> Result<(), Box> { let mut router = Router::new(); router.insert("/{file}", 1)?; router.insert("/{file}.{extension}", 1)?; @@ -40,7 +40,7 @@ fn test_depth_matching_simple() -> Result<(), Box> { } #[test] -fn test_depth_matching_complex() -> Result<(), Box> { +fn test_specific_matching_complex() -> Result<(), Box> { let mut router = Router::new(); router.insert("/{year}", 1)?; router.insert("/{year}-{month}", 1)?;