diff --git a/BENCHMARKING.md b/BENCHMARKING.md new file mode 100644 index 0000000..7c48062 --- /dev/null +++ b/BENCHMARKING.md @@ -0,0 +1,44 @@ +# Benchmarking + +All benchmarks ran on a M1 Pro laptop running Asahi Linux. + +Check out our [codspeed results](https://codspeed.io/DuskSystems/wayfind/benchmarks) for a more accurate set of timings. + +## Context + +For all benchmarks, we percent-decode the path before matching. +After matching, we convert any extracted parameters to strings. + +Some routers perform these operations automatically, while others require them to be done manually. + +We do this to try and match behaviour as best as possible. This is as close to an "apples-to-apples" comparison as we can get. + +## `matchit` inspired benches + +In a router of 130 routes, benchmark matching 4 paths. + +| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | +|:-----------------|----------:|------------:|-----------:|--------------:|-------------:| +| wayfind | 415.67 ns | 4 | 265 B | 4 | 265 B | +| matchit | 559.16 ns | 4 | 416 B | 4 | 448 B | +| path-tree | 570.10 ns | 4 | 416 B | 4 | 448 B | +| xitca-router | 650.12 ns | 7 | 800 B | 7 | 832 B | +| ntex-router | 2.2439 µs | 18 | 1.248 KB | 18 | 1.28 KB | +| route-recognizer | 3.1662 µs | 160 | 8.505 KB | 160 | 8.537 KB | +| routefinder | 6.2237 µs | 67 | 5.024 KB | 67 | 5.056 KB | +| actix-router | 21.072 µs | 214 | 13.93 KB | 214 | 13.96 KB | + +## `path-tree` inspired benches + +In a router of 320 routes, benchmark matching 80 paths. + +| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | +|:-----------------|----------:|------------:|-----------:|--------------:|-------------:| +| wayfind | 5.9508 µs | 59 | 2.567 KB | 59 | 2.567 KB | +| path-tree | 8.5983 µs | 59 | 7.447 KB | 59 | 7.47 KB | +| matchit | 9.8800 µs | 140 | 17.81 KB | 140 | 17.83 KB | +| xitca-router | 11.930 µs | 209 | 25.51 KB | 209 | 25.53 KB | +| ntex-router | 35.919 µs | 201 | 19.54 KB | 201 | 19.56 KB | +| route-recognizer | 69.604 µs | 2872 | 191.7 KB | 2872 | 204.8 KB | +| routefinder | 87.659 µs | 525 | 48.40 KB | 525 | 48.43 KB | +| actix-router | 187.49 µs | 2201 | 128.8 KB | 2201 | 128.8 KB | diff --git a/README.md b/README.md index ccc91ae..01039e6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A speedy, flexible router for Rust. -NOTE: `wayfind` is still a work in progress. +NOTE: `wayfind` is still a work in progress. ## Why another router? @@ -601,50 +601,7 @@ fn main() -> Result<(), Box> { However, as is often the case, your mileage may vary (YMMV). Benchmarks, especially micro-benchmarks, should be taken with a grain of salt. -### Benchmarks - -All benchmarks ran on a M1 Pro laptop running Asahi Linux. - -Check out our [codspeed results](https://codspeed.io/DuskSystems/wayfind/benchmarks) for a more accurate set of timings. - -#### Context - -For all benchmarks, we percent-decode the path before matching. -After matching, we convert any extracted parameters to strings. - -Some routers perform these operations automatically, while others require them to be done manually. - -We do this to try and match behaviour as best as possible. This is as close to an "apples-to-apples" comparison as we can get. - -#### `matchit` inspired benches - -In a router of 130 routes, benchmark matching 4 paths. - -| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | -|:-----------------|----------:|------------:|-----------:|--------------:|-------------:| -| wayfind | 415.67 ns | 4 | 265 B | 4 | 265 B | -| matchit | 559.16 ns | 4 | 416 B | 4 | 448 B | -| path-tree | 570.10 ns | 4 | 416 B | 4 | 448 B | -| xitca-router | 650.12 ns | 7 | 800 B | 7 | 832 B | -| ntex-router | 2.2439 µs | 18 | 1.248 KB | 18 | 1.28 KB | -| route-recognizer | 3.1662 µs | 160 | 8.505 KB | 160 | 8.537 KB | -| routefinder | 6.2237 µs | 67 | 5.024 KB | 67 | 5.056 KB | -| actix-router | 21.072 µs | 214 | 13.93 KB | 214 | 13.96 KB | - -#### `path-tree` inspired benches - -In a router of 320 routes, benchmark matching 80 paths. - -| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size | -|:-----------------|----------:|------------:|-----------:|--------------:|-------------:| -| wayfind | 5.9508 µs | 59 | 2.567 KB | 59 | 2.567 KB | -| path-tree | 8.5983 µs | 59 | 7.447 KB | 59 | 7.47 KB | -| matchit | 9.8800 µs | 140 | 17.81 KB | 140 | 17.83 KB | -| xitca-router | 11.930 µs | 209 | 25.51 KB | 209 | 25.53 KB | -| ntex-router | 35.919 µs | 201 | 19.54 KB | 201 | 19.56 KB | -| route-recognizer | 69.604 µs | 2872 | 191.7 KB | 2872 | 204.8 KB | -| routefinder | 87.659 µs | 525 | 48.40 KB | 525 | 48.43 KB | -| actix-router | 187.49 µs | 2201 | 128.8 KB | 2201 | 128.8 KB | +See [BENCHMARKING.md](BENCHMARKING.md) for the results. ## License diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e772672 --- /dev/null +++ b/TODO.md @@ -0,0 +1,10 @@ +# TODO + +- Authority router. +- Stop using the term 'route' to mean 'template'. +- Split routers into seperate crates. +- Dedupe the 2 tree routers? (Auth vs Patha) +- Consider removing expanded routes, and accepting routes as a vec? (complexity/performance issues) - would need to revamp gitlab logic too +- Improve our errors. +- Documentation refresh. +- Look into query/headers/... diff --git a/docs/Authority.md b/docs/Authority.md new file mode 100644 index 0000000..3d75ccf --- /dev/null +++ b/docs/Authority.md @@ -0,0 +1,3 @@ +# Authority Router + +TODO diff --git a/docs/Method.md b/docs/Method.md new file mode 100644 index 0000000..cf35900 --- /dev/null +++ b/docs/Method.md @@ -0,0 +1,3 @@ +# Method Router + +TODO diff --git a/docs/Path.md b/docs/Path.md new file mode 100644 index 0000000..bc46a31 --- /dev/null +++ b/docs/Path.md @@ -0,0 +1,3 @@ +# Path Router + +TODO diff --git a/src/errors.rs b/src/errors.rs index e50fc38..3539abb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -20,8 +20,11 @@ pub use route::RouteError; pub(crate) mod search; pub use search::SearchError; +pub use crate::router::authority::errors::{ + AuthorityConstraintError, AuthorityDeleteError, AuthorityInsertError, AuthoritySearchError, + AuthorityTemplateError, +}; +pub use crate::router::method::errors::{MethodDeleteError, MethodInsertError, MethodSearchError}; pub use crate::router::path::errors::{ PathConstraintError, PathDeleteError, PathInsertError, PathRouteError, PathSearchError, }; - -pub use crate::router::method::errors::{MethodDeleteError, MethodInsertError, MethodSearchError}; diff --git a/src/lib.rs b/src/lib.rs index 5b2ce29..12ed1e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub(crate) mod route; pub use route::{Route, RouteBuilder}; pub(crate) mod router; +pub use router::authority::AuthorityId; pub use router::method::MethodId; pub use router::path::{PathConstraint, PathId, PathParameters}; pub use router::{Match, MethodMatch, PathMatch, Router}; diff --git a/src/router.rs b/src/router.rs index e49bc72..5b45617 100644 --- a/src/router.rs +++ b/src/router.rs @@ -9,6 +9,7 @@ use method::MethodRouter; use path::{PathParameters, PathRouter}; use std::collections::BTreeMap; +pub mod authority; pub mod method; pub mod path; diff --git a/src/router/authority.rs b/src/router/authority.rs new file mode 100644 index 0000000..868d376 --- /dev/null +++ b/src/router/authority.rs @@ -0,0 +1,104 @@ +#![allow(dead_code)] +#![allow(clippy::needless_pass_by_ref_mut)] + +use errors::{ + AuthorityConstraintError, AuthorityDeleteError, AuthorityInsertError, AuthoritySearchError, +}; +use id::AuthorityIdGenerator; +use node::Node; +use smallvec::SmallVec; +use state::RootState; +use std::{collections::HashMap, fmt::Display}; + +pub mod constraints; +// pub mod delete; +pub mod display; +pub mod errors; +// pub mod find; +pub mod id; +// pub mod insert; +pub mod node; +// pub mod optimize; +pub mod parser; +// pub mod search; +pub mod state; + +pub use constraints::AuthorityConstraint; +pub use id::AuthorityId; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AuthorityData<'r> { + pub id: AuthorityId, + pub authority: &'r str, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct AuthorityMatch<'r, 'p> { + pub id: AuthorityId, + pub authority: &'r str, + pub parameters: AuthorityParameters<'r, 'p>, +} + +pub type AuthorityParameters<'r, 'p> = SmallVec<[(&'r str, &'p str); 4]>; + +#[derive(Clone)] +pub struct StoredConstraint { + pub type_name: &'static str, + pub check: fn(&str) -> bool, +} + +#[derive(Clone)] +pub struct Authorityauthorityr<'r> { + pub root: Node<'r, RootState>, + pub constraints: HashMap<&'r str, StoredConstraint>, + pub id: AuthorityIdGenerator, +} + +impl<'r> Authorityauthorityr<'r> { + #[must_use] + pub fn new() -> Self { + todo!() + } + + pub fn constraint(&mut self) -> Result<(), AuthorityConstraintError> { + todo!() + } + + pub(crate) fn conflicts( + &self, + _authority: &str, + ) -> Result, AuthorityDeleteError> { + todo!() + } + + pub(crate) fn insert( + &mut self, + _authority: &'r str, + ) -> Result { + todo!() + } + + pub(crate) fn find( + &self, + _authority: &str, + ) -> Result, AuthorityDeleteError> { + todo!() + } + + pub(crate) fn delete(&mut self, _authority: &str) { + todo!() + } + + pub(crate) fn search<'p>( + &'r self, + _path: &'p [u8], + ) -> Result>, AuthoritySearchError> { + todo!() + } +} + +impl Display for Authorityauthorityr<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.root) + } +} diff --git a/src/router/authority/constraints.rs b/src/router/authority/constraints.rs new file mode 100644 index 0000000..2022185 --- /dev/null +++ b/src/router/authority/constraints.rs @@ -0,0 +1,5 @@ +pub trait AuthorityConstraint: Send + Sync { + const NAME: &'static str; + + fn check(segment: &str) -> bool; +} diff --git a/src/router/authority/display.rs b/src/router/authority/display.rs new file mode 100644 index 0000000..83e6836 --- /dev/null +++ b/src/router/authority/display.rs @@ -0,0 +1,103 @@ +use crate::router::authority::{node::Node, state::State}; +use std::fmt::{Display, Write}; + +impl Display for Node<'_, S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn debug_node( + output: &mut String, + node: &Node<'_, S>, + padding: &str, + is_top: bool, + is_last: bool, + ) -> std::fmt::Result { + let key = node.state.key(); + + if is_top { + if let Some(data) = node.data.as_ref() { + writeln!(output, "{key} [{}]", data.id)?; + } else { + writeln!(output, "{key}")?; + } + } else { + let branch = if is_last { "╰─" } else { "├─" }; + if let Some(data) = node.data.as_ref() { + writeln!(output, "{padding}{branch} {key} [{}]", data.id)?; + } else { + writeln!(output, "{padding}{branch} {key}")?; + } + } + + let new_prefix = if is_top { + padding.to_owned() + } else if is_last { + format!("{padding} ") + } else { + format!("{padding}│ ") + }; + + let mut total_children = node.static_children.len() + + node.dynamic_children.len() + + node.wildcard_children.len() + + node.end_wildcard_children.len(); + + for child in node.static_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_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)?; + } + + Ok(()) + } + + let mut output = String::new(); + let padding = " ".repeat(self.state.padding()); + + // Handle root node manually + if self.state.key().is_empty() { + let total_children = self.static_children.len() + + self.dynamic_children.len() + + self.wildcard_children.len() + + self.end_wildcard_children.len(); + + let mut remaining = total_children; + + for child in self.static_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_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)?; + } + } else { + debug_node(&mut output, self, &padding, true, true)?; + } + + write!(f, "{}", output.trim_end()) + } +} diff --git a/src/router/authority/errors.rs b/src/router/authority/errors.rs new file mode 100644 index 0000000..3fa37d7 --- /dev/null +++ b/src/router/authority/errors.rs @@ -0,0 +1,14 @@ +pub mod constraint; +pub use constraint::AuthorityConstraintError; + +pub mod delete; +pub use delete::AuthorityDeleteError; + +pub mod insert; +pub use insert::AuthorityInsertError; + +pub mod search; +pub use search::AuthoritySearchError; + +pub mod template; +pub use template::AuthorityTemplateError; diff --git a/src/router/authority/errors/constraint.rs b/src/router/authority/errors/constraint.rs new file mode 100644 index 0000000..a2d31f7 --- /dev/null +++ b/src/router/authority/errors/constraint.rs @@ -0,0 +1,37 @@ +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityConstraintError { + DuplicateName { + name: &'static str, + existing_type: &'static str, + new_type: &'static str, + }, +} + +impl Error for AuthorityConstraintError {} + +impl Display for AuthorityConstraintError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DuplicateName { + name, + existing_type, + new_type, + } => write!( + f, + "duplicate constraint name + +The constraint name '{name}' is already in use: + - existing constraint type: '{existing_type}' + - new constraint type: '{new_type}' + +help: each constraint must have a unique name + +try: + - Check if you have accidentally added the same constraint twice + - Ensure different constraints have different names", + ), + } + } +} diff --git a/src/router/authority/errors/delete.rs b/src/router/authority/errors/delete.rs new file mode 100644 index 0000000..8762929 --- /dev/null +++ b/src/router/authority/errors/delete.rs @@ -0,0 +1,54 @@ +use super::AuthorityTemplateError; +use crate::errors::EncodingError; +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityDeleteError { + EncodingError(EncodingError), + TemplateError(AuthorityTemplateError), + NotFound { authority: String }, + RouteMismatch { authority: String, inserted: String }, +} + +impl Error for AuthorityDeleteError {} + +impl Display for AuthorityDeleteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EncodingError(error) => error.fmt(f), + Self::TemplateError(error) => error.fmt(f), + Self::NotFound { authority } => write!( + f, + r"not found + + Authority: {authority} + +The specified authority does not exist in the router" + ), + Self::RouteMismatch { + authority, + inserted, + } => write!( + f, + r"delete mismatch + + Authority: {authority} + Inserted: {inserted} + +The authority must be deleted using the same format as was inserted" + ), + } + } +} + +impl From for AuthorityDeleteError { + fn from(error: EncodingError) -> Self { + Self::EncodingError(error) + } +} + +impl From for AuthorityDeleteError { + fn from(error: AuthorityTemplateError) -> Self { + Self::TemplateError(error) + } +} diff --git a/src/router/authority/errors/insert.rs b/src/router/authority/errors/insert.rs new file mode 100644 index 0000000..ccc3304 --- /dev/null +++ b/src/router/authority/errors/insert.rs @@ -0,0 +1,35 @@ +use super::AuthorityTemplateError; +use crate::AuthorityId; +use std::{error::Error, fmt::Display}; + +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityInsertError { + TemplateError(AuthorityTemplateError), + Overlapping { ids: Vec }, + UnknownConstraint { constraint: String }, +} + +impl Error for AuthorityInsertError {} + +impl Display for AuthorityInsertError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::TemplateError(error) => error.fmt(f), + Self::Overlapping { ids } => write!(f, r"overlapping authorities {ids:?}"), + Self::UnknownConstraint { constraint } => write!( + f, + r"unknown constraint + + Constraint: {constraint} + +The router doesn't recognize this constraint" + ), + } + } +} + +impl From for AuthorityInsertError { + fn from(error: AuthorityTemplateError) -> Self { + Self::TemplateError(error) + } +} diff --git a/src/router/authority/errors/search.rs b/src/router/authority/errors/search.rs new file mode 100644 index 0000000..201586e --- /dev/null +++ b/src/router/authority/errors/search.rs @@ -0,0 +1,25 @@ +use crate::errors::EncodingError; +use std::{error::Error, fmt::Display}; + +/// Errors relating to attempting to search for a match in a [`Router`](crate::Router). +#[derive(Debug, PartialEq, Eq)] +pub enum AuthoritySearchError { + /// A [`EncodingError`] that occurred during the search. + EncodingError(EncodingError), +} + +impl Error for AuthoritySearchError {} + +impl Display for AuthoritySearchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EncodingError(error) => error.fmt(f), + } + } +} + +impl From for AuthoritySearchError { + fn from(error: EncodingError) -> Self { + Self::EncodingError(error) + } +} diff --git a/src/router/authority/errors/template.rs b/src/router/authority/errors/template.rs new file mode 100644 index 0000000..9be5e47 --- /dev/null +++ b/src/router/authority/errors/template.rs @@ -0,0 +1,596 @@ +use crate::errors::EncodingError; +use std::{error::Error, fmt::Display}; + +/// Errors relating to malformed routes. +#[derive(Debug, PartialEq, Eq)] +pub enum AuthorityTemplateError { + /// A [`EncodingError`] that occurred during the decoding. + EncodingError(EncodingError), + + /// The route is empty. + Empty, + + /// The route must start with '/'. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::MissingLeadingSlash { + /// route: "abc".to_string(), + /// }; + /// + /// let display = " + /// missing leading slash + /// + /// Route: abc + /// + /// tip: Routes must begin with '/' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + MissingLeadingSlash { + /// The route missing a leading slash. + route: String, + }, + + /// Empty braces were found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyBraces { + /// route: "/{}".to_string(), + /// position: 1, + /// }; + /// + /// let display = " + /// empty braces + /// + /// Route: /{} + /// ^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyBraces { + /// The route containing empty braces. + route: String, + /// The position of the first empty brace. + position: usize, + }, + + /// An unbalanced brace was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::UnbalancedBrace { + /// route: "/{".to_string(), + /// position: 1, + /// }; + /// + /// let display = " + /// unbalanced brace + /// + /// Route: /{ + /// ^ + /// + /// tip: Use '\\{' and '\\}' to represent literal '{' and '}' characters in the route + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + UnbalancedBrace { + /// The route containing an unbalanced brace. + route: String, + /// The position of the unbalanced brace. + position: usize, + }, + + /// Empty parentheses were found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyParentheses { + /// route: "/()".to_string(), + /// position: 1, + /// }; + /// + /// let display = " + /// empty parentheses + /// + /// Route: /() + /// ^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyParentheses { + /// The route containing empty parentheses. + route: String, + /// The position of the first empty parenthesis. + position: usize, + }, + + /// An unbalanced parenthesis was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::UnbalancedParenthesis { + /// route: "/(".to_string(), + /// position: 1, + /// }; + /// + /// let display = " + /// unbalanced parenthesis + /// + /// Route: /( + /// ^ + /// + /// tip: Use '\\(' and '\\)' to represent literal '(' and ')' characters in the route + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + UnbalancedParenthesis { + /// The route containing an unbalanced parenthesis. + route: String, + /// The position of the unbalanced parenthesis. + position: usize, + }, + + /// An empty parameter name was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyParameter { + /// route: "/{:}".to_string(), + /// start: 1, + /// length: 3, + /// }; + /// + /// let display = " + /// empty parameter name + /// + /// Route: /{:} + /// ^^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyParameter { + /// The route containing an empty parameter. + route: String, + /// The position of the opening brace of the empty name parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// An invalid parameter name was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::InvalidParameter { + /// route: "/{a/b}".to_string(), + /// name: "a/b".to_string(), + /// start: 1, + /// length: 5, + /// }; + /// + /// let display = " + /// invalid parameter name + /// + /// Route: /{a/b} + /// ^^^^^ + /// + /// tip: Parameter names must not contain the characters: ':', '*', '{', '}', '(', ')', '/' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidParameter { + /// The route containing an invalid parameter. + route: String, + /// The invalid parameter name. + name: String, + /// The position of the opening brace of the invalid name parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// A duplicate parameter name was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::DuplicateParameter { + /// route: "/{id}/{id}".to_string(), + /// name: "id".to_string(), + /// first: 1, + /// first_length: 4, + /// second: 6, + /// second_length: 4, + /// }; + /// + /// let display = " + /// duplicate parameter name: 'id' + /// + /// Route: /{id}/{id} + /// ^^^^ ^^^^ + /// + /// tip: Parameter names must be unique within a route + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + DuplicateParameter { + /// The route containing duplicate parameters. + route: String, + /// The duplicated parameter name. + name: String, + /// The position of the opening brace of the first occurrence. + first: usize, + /// The length of the first parameter (including braces). + first_length: usize, + /// The position of the opening brace of the second occurrence. + second: usize, + /// The length of the second parameter (including braces). + second_length: usize, + }, + + /// A wildcard parameter with no name was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyWildcard { + /// route: "/{*}".to_string(), + /// start: 1, + /// length: 3, + /// }; + /// + /// let display = " + /// empty wildcard name + /// + /// Route: /{*} + /// ^^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyWildcard { + /// The route containing an empty wildcard parameter. + route: String, + /// The position of the opening brace of the empty wildcard parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// An empty constraint name was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::EmptyConstraint { + /// route: "/{a:}".to_string(), + /// start: 1, + /// length: 4, + /// }; + /// + /// let display = " + /// empty constraint name + /// + /// Route: /{a:} + /// ^^^^ + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + EmptyConstraint { + /// The route containing an empty constraint. + route: String, + /// The position of the opening brace of the empty constraint parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// An invalid constraint name was found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::InvalidConstraint { + /// route: "/{a:b/c}".to_string(), + /// name: "b/c".to_string(), + /// start: 1, + /// length: 7, + /// }; + /// + /// let display = " + /// invalid constraint name + /// + /// Route: /{a:b/c} + /// ^^^^^^^ + /// + /// tip: Constraint names must not contain the characters: ':', '*', '{', '}', '(', ')', '/' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidConstraint { + /// The route containing an invalid constraint. + route: String, + /// The invalid constraint name. + name: String, + /// The position of the opening brace of the invalid constraint parameter. + start: usize, + /// The length of the parameter (including braces). + length: usize, + }, + + /// Two parameters side by side were found in the route. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::AuthorityTemplateError; + /// + /// let error = AuthorityTemplateError::TouchingParameters { + /// route: "/{a}{b}".to_string(), + /// start: 1, + /// length: 6, + /// }; + /// + /// let display = " + /// touching parameters + /// + /// Route: /{a}{b} + /// ^^^^^^ + /// + /// tip: Touching parameters are not supported + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + TouchingParameters { + /// The route containing touching parameters. + route: String, + /// The position of the first opening brace. + start: usize, + /// The combined length of both parameters (including braces). + length: usize, + }, +} + +impl Error for AuthorityTemplateError {} + +impl Display for AuthorityTemplateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EncodingError(error) => error.fmt(f), + Self::Empty => write!(f, "empty route"), + + Self::MissingLeadingSlash { route } => { + write!( + f, + r"missing leading slash + + Route: {route} + +tip: Routes must begin with '/'" + ) + } + + Self::EmptyBraces { route, position } => { + let arrow = " ".repeat(*position) + "^^"; + write!( + f, + r"empty braces + + Route: {route} + {arrow}" + ) + } + + Self::UnbalancedBrace { route, position } => { + let arrow = " ".repeat(*position) + "^"; + write!( + f, + r"unbalanced brace + + Route: {route} + {arrow} + +tip: Use '\{{' and '\}}' to represent literal '{{' and '}}' characters in the route" + ) + } + + Self::EmptyParentheses { route, position } => { + let arrow = " ".repeat(*position) + "^^"; + write!( + f, + r"empty parentheses + + Route: {route} + {arrow}" + ) + } + + Self::UnbalancedParenthesis { route, position } => { + let arrow = " ".repeat(*position) + "^"; + write!( + f, + r"unbalanced parenthesis + + Route: {route} + {arrow} + +tip: Use '\(' and '\)' to represent literal '(' and ')' characters in the route" + ) + } + + Self::EmptyParameter { + route, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"empty parameter name + + Route: {route} + {arrow}" + ) + } + + Self::InvalidParameter { + route, + start, + length, + .. + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"invalid parameter name + + Route: {route} + {arrow} + +tip: Parameter names must not contain the characters: ':', '*', '{{', '}}', '(', ')', '/'" + ) + } + + Self::DuplicateParameter { + route, + name, + first, + first_length, + second, + second_length, + } => { + let mut arrow = " ".repeat(route.len()); + + arrow.replace_range(*first..(*first + *first_length), &"^".repeat(*first_length)); + + arrow.replace_range( + *second..(*second + *second_length), + &"^".repeat(*second_length), + ); + + write!( + f, + r"duplicate parameter name: '{name}' + + Route: {route} + {arrow} + +tip: Parameter names must be unique within a route" + ) + } + + Self::EmptyWildcard { + route, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"empty wildcard name + + Route: {route} + {arrow}" + ) + } + + Self::EmptyConstraint { + route, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"empty constraint name + + Route: {route} + {arrow}" + ) + } + + Self::InvalidConstraint { + route, + start, + length, + .. + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"invalid constraint name + + Route: {route} + {arrow} + +tip: Constraint names must not contain the characters: ':', '*', '{{', '}}', '(', ')', '/'" + ) + } + + Self::TouchingParameters { + route, + start, + length, + } => { + let arrow = " ".repeat(*start) + &"^".repeat(*length); + write!( + f, + r"touching parameters + + Route: {route} + {arrow} + +tip: Touching parameters are not supported" + ) + } + } + } +} + +impl From for AuthorityTemplateError { + fn from(error: EncodingError) -> Self { + Self::EncodingError(error) + } +} diff --git a/src/router/authority/id.rs b/src/router/authority/id.rs new file mode 100644 index 0000000..9e8fb7e --- /dev/null +++ b/src/router/authority/id.rs @@ -0,0 +1,26 @@ +use std::fmt::Display; + +#[derive(Clone, Default)] +pub struct AuthorityIdGenerator { + id: usize, +} + +impl AuthorityIdGenerator { + pub fn next(&mut self) -> AuthorityId { + self.id += 1; + AuthorityId(Some(self.id)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct AuthorityId(pub Option); + +impl Display for AuthorityId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(id) = self.0 { + write!(f, "{id}") + } else { + write!(f, "*") + } + } +} diff --git a/src/router/authority/node.rs b/src/router/authority/node.rs new file mode 100644 index 0000000..84d4b57 --- /dev/null +++ b/src/router/authority/node.rs @@ -0,0 +1,44 @@ +use super::{ + state::{DynamicState, EndWildcardState, State, StaticState, WildcardState}, + AuthorityData, +}; +use crate::vec::SortedVec; +use std::cmp::Ordering; + +/// Represents a node in the tree structure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Node<'r, S: State> { + /// The type of Node, and associated structure data. + pub state: S, + + /// Optional data associated with this node. + /// The presence of this data is needed to successfully match a route. + pub data: Option>, + + pub static_children: SortedVec>, + pub dynamic_children: SortedVec>, + pub dynamic_children_shortcut: bool, + pub wildcard_children: SortedVec>, + pub wildcard_children_shortcut: bool, + pub end_wildcard_children: SortedVec>, + + /// Higher values indicate more specific matches. + pub priority: usize, + /// Flag indicating whether this node or its children need optimization. + pub needs_optimization: bool, +} + +impl PartialOrd for Node<'_, S> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Node<'_, S> { + fn cmp(&self, other: &Self) -> Ordering { + other + .priority + .cmp(&self.priority) + .then_with(|| self.state.cmp(&other.state)) + } +} diff --git a/src/router/authority/parser.rs b/src/router/authority/parser.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/router/authority/state.rs b/src/router/authority/state.rs new file mode 100644 index 0000000..1bc2ea0 --- /dev/null +++ b/src/router/authority/state.rs @@ -0,0 +1,279 @@ +use std::cmp::Ordering; + +pub trait State: Ord { + fn priority(&self) -> usize; + fn padding(&self) -> usize; + fn key(&self) -> &str; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RootState { + priority: usize, + padding: usize, + key: String, +} + +impl RootState { + pub const fn new() -> Self { + Self { + priority: 0, + padding: 0, + key: String::new(), + } + } +} + +impl State for RootState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for RootState { + fn cmp(&self, _: &Self) -> Ordering { + unreachable!() + } +} + +impl PartialOrd for RootState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StaticState { + pub prefix: Vec, + priority: usize, + padding: usize, + key: String, +} + +impl StaticState { + pub fn new(prefix: Vec) -> Self { + let priority = prefix.len(); + let padding = prefix.len().saturating_sub(1); + let key = String::from_utf8_lossy(&prefix).into_owned(); + + Self { + prefix, + priority, + padding, + key, + } + } +} + +impl State for StaticState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for StaticState { + fn cmp(&self, other: &Self) -> Ordering { + self.prefix.cmp(&other.prefix) + } +} + +impl PartialOrd for StaticState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DynamicState { + pub name: String, + pub constraint: Option, + priority: usize, + padding: usize, + key: String, +} + +impl DynamicState { + pub fn new(name: String, constraint: Option) -> Self { + let mut priority = name.len(); + if constraint.is_some() { + priority += 10_000; + } + + let padding = name.len().saturating_sub(1); + let key = constraint.as_ref().map_or_else( + || format!("{{{name}}}"), + |constraint| format!("{{{name}:{constraint}}}"), + ); + + Self { + name, + constraint, + priority, + padding, + key, + } + } +} + +impl State 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) + .then_with(|| self.constraint.cmp(&other.constraint)) + } +} + +impl PartialOrd for DynamicState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WildcardState { + pub name: String, + pub constraint: Option, + priority: usize, + padding: usize, + key: String, +} + +impl WildcardState { + pub fn new(name: String, constraint: Option) -> Self { + let mut priority = name.len(); + if constraint.is_some() { + priority += 10_000; + } + + let padding = name.len().saturating_sub(1); + let key = constraint.as_ref().map_or_else( + || format!("{{*{name}}}"), + |constraint| format!("{{*{name}:{constraint}}}"), + ); + + Self { + name, + constraint, + priority, + padding, + key, + } + } +} + +impl State 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) + .then_with(|| self.constraint.cmp(&other.constraint)) + } +} + +impl PartialOrd for WildcardState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EndWildcardState { + pub name: String, + pub constraint: Option, + priority: usize, + padding: usize, + key: String, +} + +impl EndWildcardState { + pub fn new(name: String, constraint: Option) -> Self { + let mut priority = name.len(); + if constraint.is_some() { + priority += 10_000; + } + + let padding = name.len().saturating_sub(1); + let key = constraint.as_ref().map_or_else( + || format!("{{*{name}}}"), + |constraint| format!("{{*{name}:{constraint}}}"), + ); + + Self { + name, + constraint, + priority, + padding, + key, + } + } +} + +impl State for EndWildcardState { + fn priority(&self) -> usize { + self.priority + } + + fn padding(&self) -> usize { + self.padding + } + + fn key(&self) -> &str { + &self.key + } +} + +impl Ord for EndWildcardState { + fn cmp(&self, other: &Self) -> Ordering { + self.name + .cmp(&other.name) + .then_with(|| self.constraint.cmp(&other.constraint)) + } +} + +impl PartialOrd for EndWildcardState { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/src/router/path/errors/delete.rs b/src/router/path/errors/delete.rs index bfb04e8..05e6921 100644 --- a/src/router/path/errors/delete.rs +++ b/src/router/path/errors/delete.rs @@ -5,9 +5,6 @@ use std::{error::Error, fmt::Display}; /// Errors relating to attempting to delete a route from a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] pub enum PathDeleteError { - /// Multiple [`PathDeleteError`] errors occurred during the delete. - Multiple(Vec), - /// A [`EncodingError`] that occurred during the decoding. EncodingError(EncodingError), @@ -76,16 +73,6 @@ impl Error for PathDeleteError {} impl Display for PathDeleteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Multiple(errors) => { - writeln!(f, "multiple path delete errors occurred:\n---\n")?; - for (index, error) in errors.iter().enumerate() { - write!(f, "{error}")?; - if index < errors.len() - 1 { - writeln!(f, "\n---\n")?; - } - } - Ok(()) - } Self::EncodingError(error) => error.fmt(f), Self::PathRouteError(error) => error.fmt(f), Self::NotFound { route } => write!(