From fdb0676f425ade89d479bfe866bd6b6edcccdb39 Mon Sep 17 00:00:00 2001 From: Cathal Mullan Date: Mon, 30 Dec 2024 16:01:26 +0000 Subject: [PATCH] Split project into multiple crates. --- .github/workflows/ci.yml | 14 +- Cargo.lock | 26 + Cargo.toml | 21 +- crates/path/Cargo.toml | 23 + .../path => crates/path/src}/constraints.rs | 0 .../router/path => crates/path/src}/delete.rs | 2 +- .../path => crates/path/src}/display.rs | 2 +- crates/path/src/errors.rs | 17 + .../path/src}/errors/constraint.rs | 6 +- .../path => crates/path/src}/errors/delete.rs | 16 +- crates/path/src/errors/encoding.rs | 106 +++ .../path => crates/path/src}/errors/insert.rs | 14 +- .../path => crates/path/src}/errors/search.rs | 8 +- .../path/src}/errors/template.rs | 8 +- {src/router/path => crates/path/src}/find.rs | 0 {src/router/path => crates/path/src}/id.rs | 2 +- .../router/path => crates/path/src}/insert.rs | 2 +- src/router/path.rs => crates/path/src/lib.rs | 32 +- {src/router/path => crates/path/src}/node.rs | 0 .../path => crates/path/src}/optimize.rs | 0 .../router/path => crates/path/src}/parser.rs | 73 +- .../router/path => crates/path/src}/search.rs | 22 +- {src/router/path => crates/path/src}/state.rs | 5 + crates/path/src/vec.rs | 82 +++ crates/percent/Cargo.toml | 19 + .../percent/src/errors.rs | 10 +- .../percent.rs => crates/percent/src/lib.rs | 52 +- crates/punycode/Cargo.toml | 19 + .../punycode/src/errors.rs | 6 +- crates/punycode/src/lib.rs | 663 ++++++++++++++++++ flake.nix | 2 +- src/decode.rs | 5 - src/errors.rs | 10 +- src/errors/delete.rs | 8 +- src/errors/encoding.rs | 18 +- src/errors/insert.rs | 8 +- src/errors/search.rs | 8 +- src/lib.rs | 4 +- src/request.rs | 11 +- src/route.rs | 13 +- src/router.rs | 7 +- src/router/path/errors.rs | 14 - 42 files changed, 1151 insertions(+), 207 deletions(-) create mode 100644 crates/path/Cargo.toml rename {src/router/path => crates/path/src}/constraints.rs (100%) rename {src/router/path => crates/path/src}/delete.rs (98%) rename {src/router/path => crates/path/src}/display.rs (98%) create mode 100644 crates/path/src/errors.rs rename {src/router/path => crates/path/src}/errors/constraint.rs (94%) rename {src/router/path => crates/path/src}/errors/delete.rs (89%) create mode 100644 crates/path/src/errors/encoding.rs rename {src/router/path => crates/path/src}/errors/insert.rs (85%) rename {src/router/path => crates/path/src}/errors/search.rs (79%) rename {src/router/path => crates/path/src}/errors/template.rs (99%) rename {src/router/path => crates/path/src}/find.rs (100%) rename {src/router/path => crates/path/src}/id.rs (90%) rename {src/router/path => crates/path/src}/insert.rs (99%) rename src/router/path.rs => crates/path/src/lib.rs (89%) rename {src/router/path => crates/path/src}/node.rs (100%) rename {src/router/path => crates/path/src}/optimize.rs (100%) rename {src/router/path => crates/path/src}/parser.rs (93%) rename {src/router/path => crates/path/src}/search.rs (93%) rename {src/router/path => crates/path/src}/state.rs (98%) create mode 100644 crates/path/src/vec.rs create mode 100644 crates/percent/Cargo.toml rename src/errors/encoding/percent.rs => crates/percent/src/errors.rs (87%) rename src/decode/percent.rs => crates/percent/src/lib.rs (86%) create mode 100644 crates/punycode/Cargo.toml rename src/errors/encoding/punycode.rs => crates/punycode/src/errors.rs (98%) create mode 100644 crates/punycode/src/lib.rs delete mode 100644 src/decode.rs delete mode 100644 src/router/path/errors.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 833ce8be..946050be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,11 +40,11 @@ jobs: run: | set -eufo pipefail cargo fmt --all --check - cargo clippy + cargo clippy --workspace cargo check --workspace cargo build --workspace - cargo test --all-targets - cargo test --doc + cargo test --workspace + cargo test --workspace --doc benchmarks: runs-on: ubuntu-24.04 @@ -64,7 +64,7 @@ jobs: cachix-auth-token: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Build benchmarks - run: cargo codspeed build + run: cargo codspeed build --workspace - name: Upload benchmark results to CodSpeed uses: CathalMullan/action@main @@ -91,7 +91,7 @@ jobs: cachix-auth-token: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Generate coverage - run: cargo llvm-cov --doctests --codecov --output-path codecov.json + run: cargo llvm-cov --workspace --doctests --codecov --output-path codecov.json - name: Upload coverage results to Codecov uses: codecov/codecov-action@v4 @@ -118,7 +118,7 @@ jobs: cachix-auth-token: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Build wayfind - run: cargo build + run: cargo build ---package wayfind wasm: runs-on: ubuntu-24.04 @@ -138,7 +138,7 @@ jobs: cachix-auth-token: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Build wayfind - run: cargo build --target wasm32-unknown-unknown + run: cargo build ---package wayfind --target wasm32-unknown-unknown oci: runs-on: ubuntu-24.04 diff --git a/Cargo.lock b/Cargo.lock index 523062b2..8804af22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1509,6 +1509,9 @@ dependencies = [ "serde_json", "similar-asserts", "smallvec", + "wayfind-path", + "wayfind-percent", + "wayfind-punycode", "wayfind-rails-macro", "xitca-router", ] @@ -1544,6 +1547,29 @@ dependencies = [ "wayfind", ] +[[package]] +name = "wayfind-path" +version = "0.7.0" +dependencies = [ + "insta", + "similar-asserts", + "smallvec", +] + +[[package]] +name = "wayfind-percent" +version = "0.7.0" +dependencies = [ + "insta", +] + +[[package]] +name = "wayfind-punycode" +version = "0.7.0" +dependencies = [ + "insta", +] + [[package]] name = "wayfind-rails" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 8a12a30a..45ae62ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,15 @@ similar.opt-level = 3 lto = "fat" codegen-units = 1 +[workspace.dependencies] +# Data Structures +smallvec = { version = "1.13", features = ["const_generics", "union"] } + +# Testing +# NOTE: Keep in sync with `cargo-insta` Nix package. +insta = "=1.41.1" +similar-asserts = "1.6" + # https://doc.rust-lang.org/cargo/reference/manifest.html [package] name = "wayfind" @@ -85,16 +94,18 @@ categories.workspace = true workspace = true [dependencies] -# Data Structures -smallvec = { version = "1.13", features = ["const_generics", "union"] } +wayfind-path = { path = "crates/path" } +wayfind-percent = { path = "crates/percent" } +wayfind-punycode = { path = "crates/punycode" } + +smallvec = { workspace = true } [dev-dependencies] wayfind-rails-macro = { path = "crates/rails-macro" } # Testing -# NOTE: Keep in sync with `cargo-insta` Nix package. -insta = "=1.41.1" -similar-asserts = "1.6" +insta = { workspace = true } +similar-asserts = { workspace = true } # Encoding percent-encoding = "2.3" diff --git a/crates/path/Cargo.toml b/crates/path/Cargo.toml new file mode 100644 index 00000000..59299695 --- /dev/null +++ b/crates/path/Cargo.toml @@ -0,0 +1,23 @@ +# https://doc.rust-lang.org/cargo/reference/manifest.html +[package] +name = "wayfind-path" +description = "Path router for `wayfind`." +publish = false + +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dependencies] +smallvec = { workspace = true } + +[dev-dependencies] +insta = { workspace = true } +similar-asserts = { workspace = true } diff --git a/src/router/path/constraints.rs b/crates/path/src/constraints.rs similarity index 100% rename from src/router/path/constraints.rs rename to crates/path/src/constraints.rs diff --git a/src/router/path/delete.rs b/crates/path/src/delete.rs similarity index 98% rename from src/router/path/delete.rs rename to crates/path/src/delete.rs index 2c7e62d3..e21d4aef 100644 --- a/src/router/path/delete.rs +++ b/crates/path/src/delete.rs @@ -2,7 +2,7 @@ use super::{ node::Node, state::{State, StaticState}, }; -use crate::router::path::parser::{ParsedTemplate, Part}; +use crate::parser::{ParsedTemplate, Part}; impl Node<'_, S> { /// Deletes a route from the node tree. diff --git a/src/router/path/display.rs b/crates/path/src/display.rs similarity index 98% rename from src/router/path/display.rs rename to crates/path/src/display.rs index fe0119d7..c3197882 100644 --- a/src/router/path/display.rs +++ b/crates/path/src/display.rs @@ -1,4 +1,4 @@ -use crate::router::path::{node::Node, state::State}; +use crate::{node::Node, state::State}; use std::fmt::{Display, Write}; impl Display for Node<'_, S> { diff --git a/crates/path/src/errors.rs b/crates/path/src/errors.rs new file mode 100644 index 00000000..0a8bcada --- /dev/null +++ b/crates/path/src/errors.rs @@ -0,0 +1,17 @@ +pub mod constraint; +pub use constraint::ConstraintError; + +pub mod delete; +pub use delete::DeleteError; + +pub mod encoding; +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/router/path/errors/constraint.rs b/crates/path/src/errors/constraint.rs similarity index 94% rename from src/router/path/errors/constraint.rs rename to crates/path/src/errors/constraint.rs index 72fe2a0d..3d3d0ad9 100644 --- a/src/router/path/errors/constraint.rs +++ b/crates/path/src/errors/constraint.rs @@ -2,7 +2,7 @@ use std::{error::Error, fmt::Display}; /// Errors relating to constraints. #[derive(Debug, PartialEq, Eq)] -pub enum PathConstraintError { +pub enum ConstraintError { /// Constraint name is already in use. /// /// # Examples @@ -42,9 +42,9 @@ pub enum PathConstraintError { }, } -impl Error for PathConstraintError {} +impl Error for ConstraintError {} -impl Display for PathConstraintError { +impl Display for ConstraintError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::DuplicateName { diff --git a/src/router/path/errors/delete.rs b/crates/path/src/errors/delete.rs similarity index 89% rename from src/router/path/errors/delete.rs rename to crates/path/src/errors/delete.rs index a1eb41c6..688ba486 100644 --- a/src/router/path/errors/delete.rs +++ b/crates/path/src/errors/delete.rs @@ -1,15 +1,15 @@ -use super::PathTemplateError; +use super::TemplateError; use crate::errors::EncodingError; 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 { +pub enum DeleteError { /// A [`EncodingError`] that occurred during the decoding. EncodingError(EncodingError), /// A [`RouteError`] that occurred during the delete. - TemplateError(PathTemplateError), + TemplateError(TemplateError), /// Route to be deleted was not found in the router. /// @@ -68,9 +68,9 @@ pub enum PathDeleteError { }, } -impl Error for PathDeleteError {} +impl Error for DeleteError {} -impl Display for PathDeleteError { +impl Display for DeleteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::EncodingError(error) => error.fmt(f), @@ -96,14 +96,14 @@ The route must be deleted using the same format as was inserted" } } -impl From for PathDeleteError { +impl From for DeleteError { fn from(error: EncodingError) -> Self { Self::EncodingError(error) } } -impl From for PathDeleteError { - fn from(error: PathTemplateError) -> Self { +impl From for DeleteError { + fn from(error: TemplateError) -> Self { Self::TemplateError(error) } } diff --git a/crates/path/src/errors/encoding.rs b/crates/path/src/errors/encoding.rs new file mode 100644 index 00000000..c105ca9f --- /dev/null +++ b/crates/path/src/errors/encoding.rs @@ -0,0 +1,106 @@ +use std::{error::Error, fmt::Display}; + +/// Errors relating to attempting to decode strings. +#[derive(Debug, PartialEq, Eq)] +pub enum EncodingError { + /// Invalid percent-encoding character encountered. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::PercentEncodingError; + /// + /// let error = PercentEncodingError::InvalidCharacter { + /// input: "/hello%GGworld".to_string(), + /// position: 6, + /// character: vec![b'%', b'G', b'G'], + /// }; + /// + /// let display = " + /// invalid character + /// + /// Input: /hello%GGworld + /// ^^^ + /// + /// Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + /// Found: '%GG' + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + InvalidCharacter { + /// The unaltered input string. + input: String, + /// The position in the input where the invalid encoding was found. + position: usize, + /// The invalid character sequence. + character: Vec, + }, + + /// Invalid UTF-8 sequence encountered. + /// + /// # Examples + /// + /// ```rust + /// use wayfind::errors::EncodingError; + /// + /// let error = EncodingError::Utf8Error { + /// input: "hello�world".to_string(), + /// }; + /// + /// let display = " + /// invalid UTF-8 sequence + /// + /// Input: hello�world + /// + /// Expected: valid UTF-8 characters + /// Found: invalid byte sequence + /// "; + /// + /// assert_eq!(error.to_string(), display.trim()); + /// ``` + Utf8Error { + /// The invalid input. + /// This will contain UTF-8 replacement symbols. + input: String, + }, +} + +impl Error for EncodingError {} + +impl Display for EncodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidCharacter { + input, + position, + character, + } => { + let character = String::from_utf8_lossy(character); + let arrow = " ".repeat(*position) + &"^".repeat(character.len()); + + write!( + f, + "invalid character + + Input: {input} + {arrow} + +Expected: '%' followed by two hexadecimal digits (a-F, 0-9) + Found: '{character}'", + ) + } + Self::Utf8Error { input } => { + write!( + f, + "invalid UTF-8 sequence + + Input: {input} + +Expected: valid UTF-8 characters + Found: invalid byte sequence", + ) + } + } + } +} diff --git a/src/router/path/errors/insert.rs b/crates/path/src/errors/insert.rs similarity index 85% rename from src/router/path/errors/insert.rs rename to crates/path/src/errors/insert.rs index 9e0fc914..40f63633 100644 --- a/src/router/path/errors/insert.rs +++ b/crates/path/src/errors/insert.rs @@ -1,12 +1,12 @@ -use super::PathTemplateError; +use super::TemplateError; use crate::PathId; use std::{error::Error, fmt::Display}; /// Errors relating to attempting to insert a route into a [`Router`](crate::Router). #[derive(Debug, PartialEq, Eq)] -pub enum PathInsertError { +pub enum InsertError { /// A [`TemplateError`] that occurred during the insert operation. - TemplateError(PathTemplateError), + TemplateError(TemplateError), /// TODO OverlappingRoutes { ids: Vec }, @@ -38,9 +38,9 @@ pub enum PathInsertError { }, } -impl Error for PathInsertError {} +impl Error for InsertError {} -impl Display for PathInsertError { +impl Display for InsertError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::TemplateError(error) => error.fmt(f), @@ -57,8 +57,8 @@ The router doesn't recognize this constraint" } } -impl From for PathInsertError { - fn from(error: PathTemplateError) -> Self { +impl From for InsertError { + fn from(error: TemplateError) -> Self { Self::TemplateError(error) } } diff --git a/src/router/path/errors/search.rs b/crates/path/src/errors/search.rs similarity index 79% rename from src/router/path/errors/search.rs rename to crates/path/src/errors/search.rs index 9a72991e..1a8f90c9 100644 --- a/src/router/path/errors/search.rs +++ b/crates/path/src/errors/search.rs @@ -3,14 +3,14 @@ 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 PathSearchError { +pub enum SearchError { /// A [`EncodingError`] that occurred during the search. EncodingError(EncodingError), } -impl Error for PathSearchError {} +impl Error for SearchError {} -impl Display for PathSearchError { +impl Display for SearchError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::EncodingError(error) => error.fmt(f), @@ -18,7 +18,7 @@ impl Display for PathSearchError { } } -impl From for PathSearchError { +impl From for SearchError { fn from(error: EncodingError) -> Self { Self::EncodingError(error) } diff --git a/src/router/path/errors/template.rs b/crates/path/src/errors/template.rs similarity index 99% rename from src/router/path/errors/template.rs rename to crates/path/src/errors/template.rs index 5f117e34..09ae777c 100644 --- a/src/router/path/errors/template.rs +++ b/crates/path/src/errors/template.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt::Display}; /// Errors relating to malformed routes. #[derive(Debug, PartialEq, Eq)] -pub enum PathTemplateError { +pub enum TemplateError { /// A [`EncodingError`] that occurred during the decoding. EncodingError(EncodingError), @@ -393,9 +393,9 @@ pub enum PathTemplateError { }, } -impl Error for PathTemplateError {} +impl Error for TemplateError {} -impl Display for PathTemplateError { +impl Display for TemplateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::EncodingError(error) => error.fmt(f), @@ -589,7 +589,7 @@ tip: Touching parameters are not supported" } } -impl From for PathTemplateError { +impl From for TemplateError { fn from(error: EncodingError) -> Self { Self::EncodingError(error) } diff --git a/src/router/path/find.rs b/crates/path/src/find.rs similarity index 100% rename from src/router/path/find.rs rename to crates/path/src/find.rs diff --git a/src/router/path/id.rs b/crates/path/src/id.rs similarity index 90% rename from src/router/path/id.rs rename to crates/path/src/id.rs index 1c0015d4..dd63b99d 100644 --- a/src/router/path/id.rs +++ b/crates/path/src/id.rs @@ -6,7 +6,7 @@ pub struct PathIdGenerator { } impl PathIdGenerator { - pub fn next(&mut self) -> PathId { + pub fn generate(&mut self) -> PathId { self.id += 1; PathId(self.id) } diff --git a/src/router/path/insert.rs b/crates/path/src/insert.rs similarity index 99% rename from src/router/path/insert.rs rename to crates/path/src/insert.rs index 76817a6d..072b3333 100644 --- a/src/router/path/insert.rs +++ b/crates/path/src/insert.rs @@ -4,7 +4,7 @@ use super::{ PathData, }; use crate::{ - router::path::parser::{ParsedTemplate, Part}, + parser::{ParsedTemplate, Part}, vec::SortedVec, }; diff --git a/src/router/path.rs b/crates/path/src/lib.rs similarity index 89% rename from src/router/path.rs rename to crates/path/src/lib.rs index d4f94cdf..c2674571 100644 --- a/src/router/path.rs +++ b/crates/path/src/lib.rs @@ -1,5 +1,7 @@ +#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] + use crate::vec::SortedVec; -use errors::{constraint::PathConstraintError, PathDeleteError, PathInsertError, PathSearchError}; +use errors::{constraint::ConstraintError, DeleteError, InsertError, SearchError}; use id::PathIdGenerator; use node::Node; use parser::{Parser, Part}; @@ -24,6 +26,7 @@ pub mod optimize; pub mod parser; pub mod search; pub mod state; +pub mod vec; pub use constraints::PathConstraint; pub use id::PathId; @@ -101,9 +104,9 @@ impl<'r> PathRouter<'r> { router } - pub fn constraint(&mut self) -> Result<(), PathConstraintError> { + pub fn constraint(&mut self) -> Result<(), ConstraintError> { if let Some(existing) = self.constraints.get(C::NAME) { - return Err(PathConstraintError::DuplicateName { + return Err(ConstraintError::DuplicateName { name: C::NAME, existing_type: existing.type_name, new_type: std::any::type_name::(), @@ -121,7 +124,7 @@ impl<'r> PathRouter<'r> { Ok(()) } - pub(crate) fn conflicts(&self, route: &str) -> Result, PathDeleteError> { + pub fn conflicts(&self, route: &str) -> Result, DeleteError> { let parsed = Parser::new(route.as_bytes())?; // Check if any expansion conflicts @@ -134,7 +137,7 @@ impl<'r> PathRouter<'r> { Ok(None) } - pub(crate) fn insert(&mut self, route: &'r str) -> Result { + pub fn insert(&mut self, route: &'r str) -> Result { let mut parsed = Parser::new(route.as_bytes())?; // Check for invalid constraints. @@ -150,7 +153,7 @@ impl<'r> PathRouter<'r> { } = part { if !self.constraints.contains_key(name.as_str()) { - return Err(PathInsertError::UnknownConstraint { + return Err(InsertError::UnknownConstraint { constraint: name.to_string(), }); } @@ -179,14 +182,14 @@ impl<'r> PathRouter<'r> { }; if ids.iter().any(|id| id != first) { - return Err(PathInsertError::OverlappingRoutes { ids }); + return Err(InsertError::OverlappingRoutes { ids }); } return Ok(*first); } // No conflicts, proceed with new insert. - let id = self.id.next(); + let id = self.id.generate(); if parsed.routes.len() > 1 { for mut parsed_route in parsed.routes { @@ -215,7 +218,7 @@ impl<'r> PathRouter<'r> { Ok(id) } - pub(crate) fn find(&self, route: &str) -> Result, PathDeleteError> { + pub fn find(&self, route: &str) -> Result, DeleteError> { let parsed = Parser::new(route.as_bytes())?; let mut id = None; @@ -230,7 +233,7 @@ impl<'r> PathRouter<'r> { if let Some(existing_id) = id { if existing_id != data.id { - return Err(PathDeleteError::Mismatch { + return Err(DeleteError::Mismatch { route: route.to_owned(), inserted: data.route.to_owned(), }); @@ -244,7 +247,7 @@ impl<'r> PathRouter<'r> { } if let Some(inserted) = mismatch { - return Err(PathDeleteError::Mismatch { + return Err(DeleteError::Mismatch { route: route.to_owned(), inserted, }); @@ -257,7 +260,7 @@ impl<'r> PathRouter<'r> { Ok(id) } - pub(crate) fn delete(&mut self, route: &str) { + pub fn delete(&mut self, route: &str) { let Ok(parsed) = Parser::new(route.as_bytes()) else { return; }; @@ -277,10 +280,7 @@ impl<'r> PathRouter<'r> { self.root.optimize(); } - pub(crate) fn search<'p>( - &'r self, - path: &'p [u8], - ) -> Result>, PathSearchError> { + pub fn search<'p>(&'r self, path: &'p [u8]) -> Result>, SearchError> { let mut parameters = smallvec![]; let Some((data, _)) = self.root.search(path, &mut parameters, &self.constraints)? else { return Ok(None); diff --git a/src/router/path/node.rs b/crates/path/src/node.rs similarity index 100% rename from src/router/path/node.rs rename to crates/path/src/node.rs diff --git a/src/router/path/optimize.rs b/crates/path/src/optimize.rs similarity index 100% rename from src/router/path/optimize.rs rename to crates/path/src/optimize.rs diff --git a/src/router/path/parser.rs b/crates/path/src/parser.rs similarity index 93% rename from src/router/path/parser.rs rename to crates/path/src/parser.rs index fe2f457e..f762c438 100644 --- a/src/router/path/parser.rs +++ b/crates/path/src/parser.rs @@ -1,4 +1,4 @@ -use super::errors::PathTemplateError; +use super::errors::TemplateError; use crate::errors::EncodingError; use smallvec::{smallvec, SmallVec}; @@ -35,9 +35,9 @@ pub struct Parser { } impl Parser { - pub fn new(input: &[u8]) -> Result { + pub fn new(input: &[u8]) -> Result { if input.is_empty() { - return Err(PathTemplateError::Empty); + return Err(TemplateError::Empty); } let routes = Self::expand_optional_groups(input, 0, input.len())?; @@ -60,7 +60,7 @@ impl Parser { input: &[u8], start: usize, end: usize, - ) -> Result>, PathTemplateError> { + ) -> Result>, TemplateError> { let mut result = Vec::from([vec![]]); let mut cursor = start; @@ -89,7 +89,7 @@ impl Parser { depth -= 1; if depth < 0 { - return Err(PathTemplateError::UnbalancedParenthesis { + return Err(TemplateError::UnbalancedParenthesis { route: String::from_utf8_lossy(input).to_string(), position: cursor, }); @@ -97,7 +97,7 @@ impl Parser { if depth == 0 { if cursor == group { - return Err(PathTemplateError::EmptyParentheses { + return Err(TemplateError::EmptyParentheses { route: String::from_utf8_lossy(input).to_string(), position: cursor - 1, }); @@ -129,7 +129,7 @@ impl Parser { } if depth != 0 { - return Err(PathTemplateError::UnbalancedParenthesis { + return Err(TemplateError::UnbalancedParenthesis { route: String::from_utf8_lossy(input).to_string(), position: start + group - 1, }); @@ -150,9 +150,9 @@ impl Parser { Ok(result) } - fn parse_route(input: &[u8], raw: &[u8]) -> Result { + fn parse_route(input: &[u8], raw: &[u8]) -> Result { if !raw.is_empty() && raw[0] != b'/' { - return Err(PathTemplateError::MissingLeadingSlash { + return Err(TemplateError::MissingLeadingSlash { route: String::from_utf8_lossy(raw).to_string(), }); } @@ -171,7 +171,7 @@ impl Parser { // Check for touching parameters. if let Some((_, start, length)) = seen_parameters.last() { if cursor == start + length { - return Err(PathTemplateError::TouchingParameters { + return Err(TemplateError::TouchingParameters { route: String::from_utf8_lossy(raw).to_string(), start: *start, length: next_cursor - start, @@ -185,7 +185,7 @@ impl Parser { .iter() .find(|(existing, _, _)| existing == name) { - return Err(PathTemplateError::DuplicateParameter { + return Err(TemplateError::DuplicateParameter { route: String::from_utf8_lossy(raw).to_string(), name: name.to_string(), first: *start, @@ -202,7 +202,7 @@ impl Parser { cursor = next_cursor; } b'}' => { - return Err(PathTemplateError::UnbalancedBrace { + return Err(TemplateError::UnbalancedBrace { route: String::from_utf8_lossy(raw).to_string(), position: cursor, }) @@ -249,10 +249,7 @@ impl Parser { (Part::Static { prefix }, end) } - fn parse_parameter_part( - input: &[u8], - cursor: usize, - ) -> Result<(Part, usize), PathTemplateError> { + fn parse_parameter_part(input: &[u8], cursor: usize) -> Result<(Part, usize), TemplateError> { let start = cursor + 1; let mut end = start; @@ -273,7 +270,7 @@ impl Parser { } if brace_count != 0 { - return Err(PathTemplateError::UnbalancedBrace { + return Err(TemplateError::UnbalancedBrace { route: String::from_utf8_lossy(input).to_string(), position: cursor, }); @@ -281,7 +278,7 @@ impl Parser { let content = &input[start..end]; if content.is_empty() { - return Err(PathTemplateError::EmptyBraces { + return Err(TemplateError::EmptyBraces { route: String::from_utf8_lossy(input).to_string(), position: cursor, }); @@ -295,7 +292,7 @@ impl Parser { }); if name.is_empty() { - return Err(PathTemplateError::EmptyParameter { + return Err(TemplateError::EmptyParameter { route: String::from_utf8_lossy(input).to_string(), start: cursor, length: end - cursor + 1, @@ -306,7 +303,7 @@ impl Parser { let name = if is_wildcard { &name[1..] } else { name }; if is_wildcard && name.is_empty() { - return Err(PathTemplateError::EmptyWildcard { + return Err(TemplateError::EmptyWildcard { route: String::from_utf8_lossy(input).to_string(), start: cursor, length: end - cursor + 1, @@ -314,7 +311,7 @@ impl Parser { } if name.iter().any(|&c| INVALID_PARAM_CHARS.contains(&c)) { - return Err(PathTemplateError::InvalidParameter { + return Err(TemplateError::InvalidParameter { route: String::from_utf8_lossy(input).to_string(), name: String::from_utf8_lossy(name).to_string(), start: cursor, @@ -324,7 +321,7 @@ impl Parser { if let Some(constraint) = constraint { if constraint.is_empty() { - return Err(PathTemplateError::EmptyConstraint { + return Err(TemplateError::EmptyConstraint { route: String::from_utf8_lossy(input).to_string(), start: cursor, length: end - cursor + 1, @@ -332,7 +329,7 @@ impl Parser { } if constraint.iter().any(|&c| INVALID_PARAM_CHARS.contains(&c)) { - return Err(PathTemplateError::InvalidConstraint { + return Err(TemplateError::InvalidConstraint { route: String::from_utf8_lossy(input).to_string(), name: String::from_utf8_lossy(name).to_string(), start: cursor, @@ -672,7 +669,7 @@ mod tests { #[test] fn test_parser_error_empty() { let error = Parser::new(b"").unwrap_err(); - assert_eq!(error, PathTemplateError::Empty); + assert_eq!(error, TemplateError::Empty); insta::assert_snapshot!(error, @"empty route"); } @@ -682,7 +679,7 @@ mod tests { let error = Parser::new(b"/users/{}").unwrap_err(); assert_eq!( error, - PathTemplateError::EmptyBraces { + TemplateError::EmptyBraces { route: "/users/{}".to_owned(), position: 7, } @@ -701,7 +698,7 @@ mod tests { let error = Parser::new(b"abc").unwrap_err(); assert_eq!( error, - PathTemplateError::MissingLeadingSlash { + TemplateError::MissingLeadingSlash { route: "abc".to_owned(), } ); @@ -720,7 +717,7 @@ mod tests { let error = Parser::new(b"/users/{id/profile").unwrap_err(); assert_eq!( error, - PathTemplateError::UnbalancedBrace { + TemplateError::UnbalancedBrace { route: "/users/{id/profile".to_owned(), position: 7, } @@ -741,7 +738,7 @@ mod tests { let error = Parser::new(b"/users/id}/profile").unwrap_err(); assert_eq!( error, - PathTemplateError::UnbalancedBrace { + TemplateError::UnbalancedBrace { route: "/users/id}/profile".to_owned(), position: 9, } @@ -762,7 +759,7 @@ mod tests { let error = Parser::new(b"/products()/category").unwrap_err(); assert_eq!( error, - PathTemplateError::EmptyParentheses { + TemplateError::EmptyParentheses { route: "/products()/category".to_owned(), position: 9, } @@ -781,7 +778,7 @@ mod tests { let error = Parser::new(b"/products(/category").unwrap_err(); assert_eq!( error, - PathTemplateError::UnbalancedParenthesis { + TemplateError::UnbalancedParenthesis { route: "/products(/category".to_owned(), position: 9, } @@ -802,7 +799,7 @@ mod tests { let error = Parser::new(b"/products)/category").unwrap_err(); assert_eq!( error, - PathTemplateError::UnbalancedParenthesis { + TemplateError::UnbalancedParenthesis { route: "/products)/category".to_owned(), position: 9, } @@ -823,7 +820,7 @@ mod tests { let error = Parser::new(b"/users/{:constraint}/profile").unwrap_err(); assert_eq!( error, - PathTemplateError::EmptyParameter { + TemplateError::EmptyParameter { route: "/users/{:constraint}/profile".to_owned(), start: 7, length: 13, @@ -843,7 +840,7 @@ mod tests { let error = Parser::new(b"/users/{user*name}/profile").unwrap_err(); assert_eq!( error, - PathTemplateError::InvalidParameter { + TemplateError::InvalidParameter { route: "/users/{user*name}/profile".to_owned(), name: "user*name".to_owned(), start: 7, @@ -866,7 +863,7 @@ mod tests { let error = Parser::new(b"/users/{id}/posts/{id:uuid}").unwrap_err(); assert_eq!( error, - PathTemplateError::DuplicateParameter { + TemplateError::DuplicateParameter { route: "/users/{id}/posts/{id:uuid}".to_owned(), name: "id".to_owned(), first: 7, @@ -891,7 +888,7 @@ mod tests { let error = Parser::new(b"/files/{*}").unwrap_err(); assert_eq!( error, - PathTemplateError::EmptyWildcard { + TemplateError::EmptyWildcard { route: "/files/{*}".to_owned(), start: 7, length: 3, @@ -911,7 +908,7 @@ mod tests { let error = Parser::new(b"/users/{id:}/profile").unwrap_err(); assert_eq!( error, - PathTemplateError::EmptyConstraint { + TemplateError::EmptyConstraint { route: "/users/{id:}/profile".to_owned(), start: 7, length: 5, @@ -931,7 +928,7 @@ mod tests { let error = Parser::new(b"/users/{id:*}/profile").unwrap_err(); assert_eq!( error, - PathTemplateError::InvalidConstraint { + TemplateError::InvalidConstraint { route: "/users/{id:*}/profile".to_owned(), name: "id".to_owned(), start: 7, @@ -954,7 +951,7 @@ mod tests { let error = Parser::new(b"/users/{id}{name}").unwrap_err(); assert_eq!( error, - PathTemplateError::TouchingParameters { + TemplateError::TouchingParameters { route: "/users/{id}{name}".to_owned(), start: 7, length: 10, diff --git a/src/router/path/search.rs b/crates/path/src/search.rs similarity index 93% rename from src/router/path/search.rs rename to crates/path/src/search.rs index 22b2a2b5..5263c700 100644 --- a/src/router/path/search.rs +++ b/crates/path/src/search.rs @@ -1,7 +1,7 @@ use super::{node::Node, state::State, PathData}; use crate::{ - errors::{EncodingError, PathSearchError}, - router::path::{PathParameters, StoredConstraint}, + errors::{EncodingError, SearchError}, + {PathParameters, StoredConstraint}, }; use smallvec::smallvec; use std::collections::HashMap; @@ -16,7 +16,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { if path.is_empty() { return Ok(self.data.as_ref().map(|data| (data, self.priority))); } @@ -45,7 +45,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { 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) @@ -67,7 +67,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { if self.dynamic_children_shortcut { self.search_dynamic_segment(path, parameters, constraints) } else { @@ -81,7 +81,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { for child in self.dynamic_children.iter() { let mut consumed = 0; @@ -135,7 +135,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { for child in self.dynamic_children.iter() { let segment_end = path.iter().position(|&b| b == b'/').unwrap_or(path.len()); @@ -166,7 +166,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { if self.wildcard_children_shortcut { self.search_wildcard_segment(path, parameters, constraints) } else { @@ -180,7 +180,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { for child in self.wildcard_children.iter() { let mut consumed = 0; @@ -230,7 +230,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { for child in self.wildcard_children.iter() { let mut consumed = 0; let mut remaining_path = path; @@ -295,7 +295,7 @@ impl<'r, S: State> Node<'r, S> { path: &'p [u8], parameters: &mut PathParameters<'r, 'p>, constraints: &HashMap<&'r str, StoredConstraint>, - ) -> Result, usize)>, PathSearchError> { + ) -> Result, usize)>, SearchError> { for child in self.end_wildcard_children.iter() { if !Self::check_constraint(child.state.constraint.as_ref(), path, constraints) { continue; diff --git a/src/router/path/state.rs b/crates/path/src/state.rs similarity index 98% rename from src/router/path/state.rs rename to crates/path/src/state.rs index 1bc2ea01..10494f30 100644 --- a/src/router/path/state.rs +++ b/crates/path/src/state.rs @@ -14,6 +14,7 @@ pub struct RootState { } impl RootState { + #[must_use] pub const fn new() -> Self { Self { priority: 0, @@ -58,6 +59,7 @@ pub struct StaticState { } impl StaticState { + #[must_use] pub fn new(prefix: Vec) -> Self { let priority = prefix.len(); let padding = prefix.len().saturating_sub(1); @@ -108,6 +110,7 @@ pub struct DynamicState { } impl DynamicState { + #[must_use] pub fn new(name: String, constraint: Option) -> Self { let mut priority = name.len(); if constraint.is_some() { @@ -168,6 +171,7 @@ pub struct WildcardState { } impl WildcardState { + #[must_use] pub fn new(name: String, constraint: Option) -> Self { let mut priority = name.len(); if constraint.is_some() { @@ -228,6 +232,7 @@ pub struct EndWildcardState { } impl EndWildcardState { + #[must_use] pub fn new(name: String, constraint: Option) -> Self { let mut priority = name.len(); if constraint.is_some() { diff --git a/crates/path/src/vec.rs b/crates/path/src/vec.rs new file mode 100644 index 00000000..035b7254 --- /dev/null +++ b/crates/path/src/vec.rs @@ -0,0 +1,82 @@ +use std::ops::{Index, IndexMut}; + +/// A `vec` which caches its sort state. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SortedVec { + vec: Vec, + sorted: bool, +} + +impl SortedVec { + #[must_use] + pub const fn new(vec: Vec) -> Self { + Self { vec, sorted: false } + } + + pub fn push(&mut self, value: T) { + self.vec.push(value); + self.sorted = false; + } + + pub fn remove(&mut self, index: usize) -> T { + self.vec.remove(index) + } + + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.vec.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + pub fn find_mut(&mut self, predicate: F) -> Option<&mut T> + where + F: Fn(&T) -> bool, + { + self.vec.iter_mut().find(|item| predicate(item)) + } + + pub fn iter(&self) -> impl Iterator { + self.vec.iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.vec.iter_mut() + } + + pub fn sort(&mut self) { + if self.sorted { + return; + } + + self.vec.sort(); + self.sorted = true; + } +} + +impl Default for SortedVec { + fn default() -> Self { + Self { + vec: vec![], + sorted: false, + } + } +} + +impl Index for SortedVec { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + &self.vec[index] + } +} + +impl IndexMut for SortedVec { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.vec[index] + } +} diff --git a/crates/percent/Cargo.toml b/crates/percent/Cargo.toml new file mode 100644 index 00000000..b2847493 --- /dev/null +++ b/crates/percent/Cargo.toml @@ -0,0 +1,19 @@ +# https://doc.rust-lang.org/cargo/reference/manifest.html +[package] +name = "wayfind-percent" +description = "Percent decoder for `wayfind`." +publish = false + +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dev-dependencies] +insta = { workspace = true } diff --git a/src/errors/encoding/percent.rs b/crates/percent/src/errors.rs similarity index 87% rename from src/errors/encoding/percent.rs rename to crates/percent/src/errors.rs index 9ba21f6f..7fc0fd9a 100644 --- a/src/errors/encoding/percent.rs +++ b/crates/percent/src/errors.rs @@ -1,15 +1,15 @@ use std::{error::Error, fmt::Display}; #[derive(Debug, PartialEq, Eq)] -pub enum PercentEncodingError { +pub enum DecodingError { /// Invalid percent-encoding character encountered. /// /// # Examples /// /// ```rust - /// use wayfind::errors::PercentEncodingError; + /// use wayfind::errors::DecodingError; /// - /// let error = PercentEncodingError::InvalidCharacter { + /// let error = DecodingError::InvalidCharacter { /// input: "/hello%GGworld".to_string(), /// position: 6, /// character: vec![b'%', b'G', b'G'], @@ -37,9 +37,9 @@ pub enum PercentEncodingError { }, } -impl Error for PercentEncodingError {} +impl Error for DecodingError {} -impl Display for PercentEncodingError { +impl Display for DecodingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InvalidCharacter { diff --git a/src/decode/percent.rs b/crates/percent/src/lib.rs similarity index 86% rename from src/decode/percent.rs rename to crates/percent/src/lib.rs index 97ebbd86..55969289 100644 --- a/src/decode/percent.rs +++ b/crates/percent/src/lib.rs @@ -1,11 +1,15 @@ //! -use crate::errors::{EncodingError, PercentEncodingError}; +#![allow(clippy::missing_errors_doc)] + +use errors::DecodingError; use std::borrow::Cow; +pub mod errors; + /// Try and percent-decode input bytes. /// Does not do any sort of normalization, simply decodes hex characters. -pub fn percent_decode(input: &[u8]) -> Result, EncodingError> { +pub fn percent_decode(input: &[u8]) -> Result, DecodingError> { if !input.contains(&b'%') { return Ok(Cow::Borrowed(input)); } @@ -17,13 +21,11 @@ pub fn percent_decode(input: &[u8]) -> Result, EncodingError> { while i < len { match input[i] { b'%' if i + 2 >= len => { - return Err(EncodingError::Percent( - PercentEncodingError::InvalidCharacter { - input: String::from_utf8_lossy(input).to_string(), - position: i, - character: input[i..].to_vec(), - }, - )); + return Err(DecodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: i, + character: input[i..].to_vec(), + }); } b'%' => { let a = input[i + 1]; @@ -32,13 +34,11 @@ pub fn percent_decode(input: &[u8]) -> Result, EncodingError> { if let Some(decoded) = decode_hex(a, b) { output.push(decoded); } else { - return Err(EncodingError::Percent( - PercentEncodingError::InvalidCharacter { - input: String::from_utf8_lossy(input).to_string(), - position: i, - character: vec![b'%', a, b], - }, - )); + return Err(DecodingError::InvalidCharacter { + input: String::from_utf8_lossy(input).to_string(), + position: i, + character: vec![b'%', a, b], + }); } i += 3; @@ -142,11 +142,11 @@ mod tests { assert_eq!( result, - EncodingError::Percent(PercentEncodingError::InvalidCharacter { + DecodingError::InvalidCharacter { input: String::from_utf8_lossy(input).to_string(), position: 0, character: vec![b'%'], - }) + } ); } @@ -168,11 +168,11 @@ mod tests { assert_eq!( result, - EncodingError::Percent(PercentEncodingError::InvalidCharacter { + DecodingError::InvalidCharacter { input: String::from_utf8_lossy(input).to_string(), position: 0, character: vec![b'%', b'a'], - }) + } ); } @@ -194,11 +194,11 @@ mod tests { assert_eq!( result, - EncodingError::Percent(PercentEncodingError::InvalidCharacter { + DecodingError::InvalidCharacter { input: String::from_utf8_lossy(input).to_string(), position: 0, character: vec![b'%', b'1'], - }) + } ); } @@ -220,11 +220,11 @@ mod tests { assert_eq!( result, - EncodingError::Percent(PercentEncodingError::InvalidCharacter { + DecodingError::InvalidCharacter { input: String::from_utf8_lossy(input).to_string(), position: 6, character: vec![b'%', b'6'], - }) + } ); } @@ -246,11 +246,11 @@ mod tests { assert_eq!( result, - EncodingError::Percent(PercentEncodingError::InvalidCharacter { + DecodingError::InvalidCharacter { input: String::from_utf8_lossy(input).to_string(), position: 0, character: vec![b'%', b'z', b'z'], - }) + } ); } diff --git a/crates/punycode/Cargo.toml b/crates/punycode/Cargo.toml new file mode 100644 index 00000000..23dffa26 --- /dev/null +++ b/crates/punycode/Cargo.toml @@ -0,0 +1,19 @@ +# https://doc.rust-lang.org/cargo/reference/manifest.html +[package] +name = "wayfind-punycode" +description = "Punycode decoder for `wayfind`." +publish = false + +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dev-dependencies] +insta = { workspace = true } diff --git a/src/errors/encoding/punycode.rs b/crates/punycode/src/errors.rs similarity index 98% rename from src/errors/encoding/punycode.rs rename to crates/punycode/src/errors.rs index df96ce8d..9bcf37d5 100644 --- a/src/errors/encoding/punycode.rs +++ b/crates/punycode/src/errors.rs @@ -1,7 +1,7 @@ use std::{error::Error, fmt::Display}; #[derive(Debug, PartialEq, Eq)] -pub enum PunycodeEncodingError { +pub enum DecodingError { /// Invalid basic code point encountered (non-ASCII character). /// /// # Examples @@ -165,9 +165,9 @@ pub enum PunycodeEncodingError { }, } -impl Error for PunycodeEncodingError {} +impl Error for DecodingError {} -impl Display for PunycodeEncodingError { +impl Display for DecodingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InvalidBasicCodePoint { diff --git a/crates/punycode/src/lib.rs b/crates/punycode/src/lib.rs new file mode 100644 index 00000000..576ea112 --- /dev/null +++ b/crates/punycode/src/lib.rs @@ -0,0 +1,663 @@ +//! + +#![allow(clippy::many_single_char_names)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::cast_possible_truncation)] + +use errors::DecodingError; +use std::borrow::Cow; + +pub mod errors; + +/// +const BASE: u32 = 36; +const TMIN: u32 = 1; +const TMAX: u32 = 26; +const SKEW: u32 = 38; +const DAMP: u32 = 700; +const INITIAL_BIAS: u32 = 72; +const INITIAL_N: u32 = 128; + +pub fn punycode_decode(input: &[u8]) -> Result, DecodingError> { + if input.is_empty() { + return Ok(String::from_utf8_lossy(input)); + } + + let mut parts = vec![]; + let mut start = 0; + + for (i, &byte) in input.iter().enumerate() { + if byte == b'.' { + if start != i { + parts.push(&input[start..i]); + } + + parts.push(&input[i..=i]); + start = i + 1; + } + } + + if start < input.len() { + parts.push(&input[start..]); + } + + let mut result = String::with_capacity(input.len()); + for part in parts { + if part == b"." { + result.push('.'); + continue; + } + + if part.starts_with(b"xn--") { + let decoded = punycode_decode_part(&part[4..])?; + result.push_str(&decoded); + } else { + let string = String::from_utf8_lossy(part); + if string.contains(|c: char| c.is_ascii_control()) { + return Err(DecodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: part.iter().position(|&x| x < 32).unwrap_or(0), + character: vec![], + }); + } + + result.push_str(&string); + } + } + + Ok(Cow::Owned(result)) +} + +/// TODO: I'd like to understand this better, and maybe enforce certain restirctions to improve performance/remove error cases. +/// +fn punycode_decode_part(input: &[u8]) -> Result { + if input == b"-" { + return Err(DecodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: 0, + }); + } + + let mut output = Vec::with_capacity(input.len()); + + let mut n: u32 = INITIAL_N; + let mut i: u32 = 0; + let mut bias: u32 = INITIAL_BIAS; + + let last_delimiter = input.iter().rposition(|&x| x == b'-').unwrap_or(0); + for &byte in &input[..last_delimiter] { + output.push(byte as char); + } + + let mut position = last_delimiter; + if last_delimiter > 0 { + position += 1; + } + + while position < input.len() { + let old_i: u32 = i; + let mut w: u32 = 1; + let mut k: u32 = BASE; + + loop { + if position >= input.len() { + return Err(DecodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: position - 1, + }); + } + + let byte = input[position]; + if !is_valid_punycode_digit(byte) { + return Err(DecodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position, + character: vec![], + }); + } + + let digit = decode_digit(byte).unwrap(); + + if k > u32::MAX - BASE { + return Err(DecodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position, + }); + } + + i = i + .checked_add( + digit + .checked_mul(w) + .ok_or_else(|| DecodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position, + })?, + ) + .ok_or_else(|| DecodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position, + })?; + + let t: u32 = if k <= bias { + TMIN + } else if k >= bias + TMAX { + TMAX + } else { + k - bias + }; + + if digit < t { + break; + } + + w = w + .checked_mul(BASE - t) + .ok_or_else(|| DecodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position, + })?; + + k += BASE; + position += 1; + } + + bias = adapt(i - old_i, output.len() as u32 + 1, old_i == 0); + + n = n + .checked_add(i / (output.len() as u32 + 1)) + .ok_or_else(|| DecodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position, + })?; + + if n > 0x0010_FFFF { + return Err(DecodingError::InvalidCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position, + value: n, + }); + } + + i %= output.len() as u32 + 1; + + if n < 128 { + return Err(DecodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position, + character: vec![], + }); + } + + let code_point = char::from_u32(n).ok_or_else(|| DecodingError::InvalidCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position, + value: n, + })?; + + output.insert(i as usize, code_point); + i += 1; + position += 1; + } + + Ok(output.into_iter().collect()) +} + +const fn is_valid_punycode_digit(cp: u8) -> bool { + matches!(cp, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9') +} + +const fn decode_digit(cp: u8) -> Option { + match cp { + b'A'..=b'Z' => Some(cp as u32 - b'A' as u32), + b'a'..=b'z' => Some(cp as u32 - b'a' as u32), + b'0'..=b'9' => Some(cp as u32 - b'0' as u32 + 26), + _ => None, + } +} + +const fn adapt(delta: u32, num_points: u32, first_time: bool) -> u32 { + let mut delta = if first_time { delta / DAMP } else { delta >> 1 }; + + delta += delta / num_points; + + let mut k = 0; + let base_minus_tmin = BASE - TMIN; + + while delta > ((base_minus_tmin * TMAX) >> 1) { + delta /= base_minus_tmin; + k += BASE; + } + + k + (((base_minus_tmin + 1) * delta) / (delta + SKEW)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// + #[test] + fn test_punycode_rfc_arabic() { + let input = b"egbpdaj6bu4bxfgehfvwxn"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "ليهمابتكلموشعربي؟"); + } + + /// + #[test] + fn test_punycode_rfc_chinese_simplified() { + let input = b"ihqwcrb4cv8a8dqg056pqjye"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "他们为什么不说中文"); + } + + /// + #[test] + fn test_punycode_rfc_chinese_traditional() { + let input = b"ihqwctvzc91f659drss3x8bo0yb"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "他們爲什麽不說中文"); + } + + /// + #[test] + fn test_punycode_rfc_czech() { + let input = b"Proprostnemluvesky-uyb24dma41a"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "Pročprostěnemluvíčesky"); + } + + /// + #[test] + fn test_punycode_rfc_hebrew() { + let input = b"4dbcagdahymbxekheh6e0a7fei0b"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "למההםפשוטלאמדבריםעברית"); + } + + /// + #[test] + fn test_punycode_rfc_hindi() { + let input = b"i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "यहलोगहिन्दीक्योंनहींबोलसकतेहैं"); + } + + /// + #[test] + fn test_punycode_rfc_japanese() { + let input = b"n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "なぜみんな日本語を話してくれないのか"); + } + + /// + #[test] + fn test_punycode_rfc_korean() { + let input = b"989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5jpsd879ccm6fea98c"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "세계의모든사람들이한국어를이해한다면얼마나좋을까"); + } + + /// + #[test] + fn test_punycode_rfc_russian() { + let input = b"b1abfaaepdrnnbgefbaDotcwatmq2g4l"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "почемужеонинеговорятпорусски"); + } + + /// + #[test] + fn test_punycode_rfc_spanish() { + let input = b"PorqunopuedensimplementehablarenEspaol-fmd56a"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "PorquénopuedensimplementehablarenEspañol"); + } + + /// + #[test] + fn test_punycode_rfc_vietnamese() { + let input = b"TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "TạisaohọkhôngthểchỉnóitiếngViệt"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_1() { + let input = b"3B-ww4c5e180e575a65lsy2b"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "3年B組金八先生"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_2() { + let input = b"-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "安室奈美恵-with-SUPER-MONKEYS"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_3() { + let input = b"Hello-Another-Way--fc4qua05auwb3674vfr0b"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "Hello-Another-Way-それぞれの場所"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_4() { + let input = b"2-u9tlzr9756bt3uc0v"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "ひとつ屋根の下2"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_5() { + let input = b"MajiKoi5-783gue6qz075azm5e"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "MajiでKoiする5秒前"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_6() { + let input = b"de-jg4avhby1noc0d"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "パフィーdeルンバ"); + } + + /// + #[test] + fn test_punycode_rfc_japanese_artist_7() { + let input = b"d9juau41awczczp"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "そのスピードで"); + } + + /// + #[test] + fn test_punycode_rfc_ascii() { + let input = b"-> $1.00 <--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-> $1.00 <-"); + } + + /// + #[test] + fn test_punycode_empty_string() { + let input = b""; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, ""); + } + + /// + #[test] + fn test_punycode_hyphen() { + let input = b"--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-"); + } + + /// + #[test] + fn test_punycode_hyphen_a() { + let input = b"-a-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-a"); + } + + /// + #[test] + fn test_punycode_hyphen_a_hyphen() { + let input = b"-a--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "-a-"); + } + + /// + #[test] + fn test_punycode_a() { + let input = b"a-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "a"); + } + + /// + #[test] + fn test_punycode_a_hyphen() { + let input = b"a--"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "a-"); + } + + /// + #[test] + fn test_punycode_a_b() { + let input = b"a-b-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "a-b"); + } + + /// + #[test] + fn test_punycode_books() { + let input = b"books-"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "books"); + } + + /// + #[test] + fn test_punycode_german() { + let input = b"bcher-kva"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "bücher"); + } + + /// + #[test] + fn test_punycode_chinese() { + let input = b"Hello-ck1hg65u"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "Hello世界"); + } + + /// + #[test] + fn test_punycode_umlaut() { + let input = b"tda"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "ü"); + } + + /// + #[test] + fn test_punycode_two_special() { + let input = b"tdac"; + let result = punycode_decode_part(input).unwrap(); + assert_eq!(result, "üý"); + } + + /// + #[test] + fn test_punycode_error_single_hyphen() { + let input = b"-"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + unexpected end of input + + Input: - + ^ + + Expected: more punycode digits + Found: end of input + "); + + assert_eq!( + result, + DecodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: 0 + } + ); + } + + /// + #[test] + fn test_punycode_error_null_byte() { + let input = b"foo\0bar"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid basic code point + + Input: foobar + ^ + + Expected: ASCII character (0-127) + Found: '' + "); + + assert_eq!( + result, + DecodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 3, + character: vec![], + } + ); + } + + /// + #[test] + fn test_punycode_error_hash() { + let input = b"foo#bar"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid basic code point + + Input: foo#bar + ^ + + Expected: ASCII character (0-127) + Found: '' + "); + + assert_eq!( + result, + DecodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 3, + character: vec![], + } + ); + } + + /// + #[test] + fn test_punycode_error_pound_symbol() { + let input = b"foo\xC2\xA3bar"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid basic code point + + Input: foo£bar + ^ + + Expected: ASCII character (0-127) + Found: '' + "); + + assert_eq!( + result, + DecodingError::InvalidBasicCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 3, + character: vec![], + } + ); + } + + /// + #[test] + fn test_punycode_error_truncated() { + let input = b"9"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + unexpected end of input + + Input: 9 + ^ + + Expected: more punycode digits + Found: end of input + "); + + assert_eq!( + result, + DecodingError::UnexpectedEnd { + input: String::from_utf8_lossy(input).to_string(), + position: 0 + } + ); + } + + /// + #[test] + fn test_punycode_error_code_point_too_large() { + let input = b"99999a"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + invalid code point + + Input: 99999a + ^ + + Cannot convert value 4760513 to valid Unicode character + "); + + assert_eq!( + result, + DecodingError::InvalidCodePoint { + input: String::from_utf8_lossy(input).to_string(), + position: 5, + value: 0x0048_A3C1, + } + ); + } + + /// + #[test] + fn test_punycode_error_overflow() { + let input = b"9999999999a"; + let result = punycode_decode_part(input).unwrap_err(); + + insta::assert_snapshot!(result, @r" + numeric overflow + + Input: 9999999999a + ^ + + Overflow occurred while decoding punycode digits + "); + + assert_eq!( + result, + DecodingError::Overflow { + input: String::from_utf8_lossy(input).to_string(), + position: 10, + } + ); + } +} diff --git a/flake.nix b/flake.nix index 2226e0d7..a0a1cc10 100644 --- a/flake.nix +++ b/flake.nix @@ -55,7 +55,7 @@ NIX_PATH = "nixpkgs=${nixpkgs.outPath}"; RUSTC_WRAPPER = "sccache"; - RUSTFLAGS = "-C target-cpu=native"; + # RUSTFLAGS = "-C target-cpu=native"; CARGO_INCREMENTAL = "0"; OCI_ROOT_URL = "http://127.0.0.1:8000"; diff --git a/src/decode.rs b/src/decode.rs deleted file mode 100644 index 8cabdd10..00000000 --- a/src/decode.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod percent; -pub use percent::percent_decode; - -mod punycode; -pub use punycode::punycode_decode; diff --git a/src/errors.rs b/src/errors.rs index 1917d85c..59028b02 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,7 +6,9 @@ pub(crate) mod delete; pub use delete::DeleteError; pub(crate) mod encoding; -pub use encoding::{EncodingError, PercentEncodingError, PunycodeEncodingError}; +pub use encoding::EncodingError; +pub use wayfind_percent::errors::DecodingError as PercentDecodingError; +pub use wayfind_punycode::errors::DecodingError as PunycodeDecodingError; pub(crate) mod insert; pub use insert::InsertError; @@ -25,6 +27,8 @@ pub use crate::router::authority::errors::{ AuthorityTemplateError, }; pub use crate::router::method::errors::{MethodDeleteError, MethodInsertError, MethodSearchError}; -pub use crate::router::path::errors::{ - PathConstraintError, PathDeleteError, PathInsertError, PathSearchError, PathTemplateError, +pub use wayfind_path::errors::{ + ConstraintError as PathConstraintError, DeleteError as PathDeleteError, + InsertError as PathInsertError, SearchError as PathSearchError, + TemplateError as PathTemplateError, }; diff --git a/src/errors/delete.rs b/src/errors/delete.rs index bd2e579f..d04b0e88 100644 --- a/src/errors/delete.rs +++ b/src/errors/delete.rs @@ -1,11 +1,11 @@ -use super::{AuthorityDeleteError, MethodDeleteError, PathDeleteError}; +use super::{AuthorityDeleteError, MethodDeleteError}; 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 DeleteError { Authority(AuthorityDeleteError), - Path(PathDeleteError), + Path(wayfind_path::errors::DeleteError), Method(MethodDeleteError), NotFound, } @@ -29,8 +29,8 @@ impl From for DeleteError { } } -impl From for DeleteError { - fn from(error: PathDeleteError) -> Self { +impl From for DeleteError { + fn from(error: wayfind_path::errors::DeleteError) -> Self { Self::Path(error) } } diff --git a/src/errors/encoding.rs b/src/errors/encoding.rs index 5dc75b39..a2cc071a 100644 --- a/src/errors/encoding.rs +++ b/src/errors/encoding.rs @@ -1,16 +1,10 @@ use std::{error::Error, fmt::Display}; -mod percent; -mod punycode; - -pub use percent::PercentEncodingError; -pub use punycode::PunycodeEncodingError; - /// Errors relating to attempting to decode strings. #[derive(Debug, PartialEq, Eq)] pub enum EncodingError { - Percent(PercentEncodingError), - Punycode(PunycodeEncodingError), + Percent(wayfind_percent::errors::DecodingError), + Punycode(wayfind_punycode::errors::DecodingError), /// Invalid UTF-8 sequence encountered. /// @@ -63,14 +57,14 @@ Expected: valid UTF-8 characters } } -impl From for EncodingError { - fn from(error: PercentEncodingError) -> Self { +impl From for EncodingError { + fn from(error: wayfind_percent::errors::DecodingError) -> Self { Self::Percent(error) } } -impl From for EncodingError { - fn from(error: PunycodeEncodingError) -> Self { +impl From for EncodingError { + fn from(error: wayfind_punycode::errors::DecodingError) -> Self { Self::Punycode(error) } } diff --git a/src/errors/insert.rs b/src/errors/insert.rs index d2b44f86..fd0774d2 100644 --- a/src/errors/insert.rs +++ b/src/errors/insert.rs @@ -1,4 +1,4 @@ -use super::{AuthorityInsertError, MethodInsertError, PathInsertError}; +use super::{AuthorityInsertError, MethodInsertError}; use crate::chain::DataChain; use std::{error::Error, fmt::Display}; @@ -6,7 +6,7 @@ use std::{error::Error, fmt::Display}; #[derive(Debug, PartialEq, Eq)] pub enum InsertError { Authority(AuthorityInsertError), - Path(PathInsertError), + Path(wayfind_path::errors::InsertError), Method(MethodInsertError), Conflict { chain: DataChain }, } @@ -35,8 +35,8 @@ impl From for InsertError { } } -impl From for InsertError { - fn from(error: PathInsertError) -> Self { +impl From for InsertError { + fn from(error: wayfind_path::errors::InsertError) -> Self { Self::Path(error) } } diff --git a/src/errors/search.rs b/src/errors/search.rs index bbef1d6f..1dc5513c 100644 --- a/src/errors/search.rs +++ b/src/errors/search.rs @@ -1,11 +1,11 @@ -use super::{AuthoritySearchError, MethodSearchError, PathSearchError}; +use super::{AuthoritySearchError, MethodSearchError}; 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 SearchError { Authority(AuthoritySearchError), - Path(PathSearchError), + Path(wayfind_path::errors::SearchError), Method(MethodSearchError), } @@ -27,8 +27,8 @@ impl From for SearchError { } } -impl From for SearchError { - fn from(error: PathSearchError) -> Self { +impl From for SearchError { + fn from(error: wayfind_path::errors::SearchError) -> Self { Self::Path(error) } } diff --git a/src/lib.rs b/src/lib.rs index 62e96bc5..26a5bc39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,6 @@ pub(crate) mod chain; pub use chain::DataChain; -pub(crate) mod decode; - pub mod errors; pub(crate) mod request; @@ -16,7 +14,7 @@ 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::{AuthorityMatch, Match, MethodMatch, PathMatch, Router}; +pub use wayfind_path::{PathConstraint, PathId, PathParameters}; pub(crate) mod vec; diff --git a/src/request.rs b/src/request.rs index 1f96ec05..a0beffb1 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,8 +1,7 @@ -use crate::{ - decode::{percent_decode, punycode_decode}, - errors::RequestError, -}; +use crate::errors::{EncodingError, RequestError}; use std::{borrow::Cow, fmt::Debug}; +use wayfind_percent::percent_decode; +use wayfind_punycode::punycode_decode; #[derive(Clone, Eq, PartialEq)] pub struct Request<'r> { @@ -76,13 +75,13 @@ impl<'p> RequestBuilder<'p> { #[allow(clippy::missing_errors_doc)] pub fn build(self) -> Result, RequestError> { let authority = if let Some(authority) = self.authority { - Some(punycode_decode(authority.as_bytes())?) + Some(punycode_decode(authority.as_bytes()).map_err(EncodingError::from)?) } else { None }; let path = self.path.ok_or(RequestError::MissingPath)?; - let path = percent_decode(path.as_bytes())?; + let path = percent_decode(path.as_bytes()).map_err(EncodingError::from)?; Ok(Request { authority, diff --git a/src/route.rs b/src/route.rs index 8a7155ce..e58987ad 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,7 +1,7 @@ -use crate::{ - decode::{percent_decode, punycode_decode}, - errors::RouteError, -}; +use wayfind_percent::percent_decode; +use wayfind_punycode::punycode_decode; + +use crate::errors::{EncodingError, RouteError}; /// A route that can be inserted into a [`Router`](`crate::Router`). #[derive(Debug, Clone, PartialEq, Eq)] @@ -54,7 +54,8 @@ impl<'r> RouteBuilder<'r> { /// Return a [`RouteError`] if a required field was not populated. pub fn build(self) -> Result, RouteError> { if let Some(authority) = self.authority { - let decoded = punycode_decode(authority.as_bytes())?; + let decoded = + punycode_decode(authority.as_bytes()).map_err(EncodingError::from)?; if authority != decoded { return Err(RouteError::EncodedAuthority { input: authority.to_owned(), @@ -66,7 +67,7 @@ impl<'r> RouteBuilder<'r> { let route = self.route.ok_or(RouteError::MissingRoute)?; // Verify path is percent-decoded - let decoded = percent_decode(route.as_bytes())?; + let decoded = percent_decode(route.as_bytes()).map_err(EncodingError::from)?; if route.as_bytes() != decoded.as_ref() { return Err(RouteError::EncodedPath { input: route.to_owned(), diff --git a/src/router.rs b/src/router.rs index ef962e26..2dff611c 100644 --- a/src/router.rs +++ b/src/router.rs @@ -7,12 +7,11 @@ use crate::{ }; use authority::{AuthorityParameters, AuthorityRouter}; use method::MethodRouter; -use path::{PathParameters, PathRouter}; use std::collections::BTreeMap; +use wayfind_path::{PathParameters, PathRouter}; pub mod authority; pub mod method; -pub mod path; #[derive(Debug, Eq, PartialEq)] pub struct Match<'r, 'p, T> { @@ -44,8 +43,8 @@ pub struct PathMatch<'r, 'p> { pub parameters: PathParameters<'r, 'p>, } -impl<'r, 'p> From> for PathMatch<'r, 'p> { - fn from(value: path::PathMatch<'r, 'p>) -> Self { +impl<'r, 'p> From> for PathMatch<'r, 'p> { + fn from(value: wayfind_path::PathMatch<'r, 'p>) -> Self { Self { route: value.route, expanded: value.expanded, diff --git a/src/router/path/errors.rs b/src/router/path/errors.rs deleted file mode 100644 index a2727305..00000000 --- a/src/router/path/errors.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod constraint; -pub use constraint::PathConstraintError; - -pub mod delete; -pub use delete::PathDeleteError; - -pub mod insert; -pub use insert::PathInsertError; - -pub mod search; -pub use search::PathSearchError; - -pub mod template; -pub use template::PathTemplateError;