From 8e66ae9d04478213b0a5a2e1c810e81d0877748c Mon Sep 17 00:00:00 2001 From: Andrey Mak Date: Mon, 14 Oct 2024 05:25:49 +0400 Subject: [PATCH] add `Router::merge` I am sure it is not the best way to implement such functionality, but since usually routes are defined at server startup there should be no significant overhead. --- src/error.rs | 24 +++++++++++++++++++ src/router.rs | 34 ++++++++++++++++++++++++++ src/tree.rs | 23 ++++++++++++++++++ tests/merge.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 tests/merge.rs diff --git a/src/error.rs b/src/error.rs index ab879f3..5f46f4b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,7 @@ use crate::escape::{UnescapedRef, UnescapedRoute}; use crate::tree::{denormalize_params, Node}; use std::fmt; +use std::ops::Deref; /// Represents errors that can occur when inserting a new route. #[non_exhaustive] @@ -97,6 +98,29 @@ impl InsertError { } } +/// A failed merge attempt. +#[derive(Debug, Clone)] +pub struct MergeError(pub(crate) Vec); + +impl fmt::Display for MergeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for error in self.0.iter() { + writeln!(f, "{}", error)?; + } + Ok(()) + } +} + +impl std::error::Error for MergeError {} + +impl Deref for MergeError { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// A failed match attempt. /// /// ``` diff --git a/src/router.rs b/src/router.rs index 8256c4b..2ed4c50 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,3 +1,4 @@ +use crate::error::MergeError; use crate::tree::Node; use crate::{InsertError, MatchError, Params}; @@ -133,6 +134,39 @@ impl Router { pub fn check_priorities(&self) -> Result { self.root.check_priorities() } + + /// Merge a given router into current one. + /// Returns a list of [`InsertError`] for every failed insertion. + /// # Examples + /// + /// ```rust + /// # use matchit::Router; + /// # fn main() -> Result<(), Box> { + /// let mut root = Router::new(); + /// root.insert("/home", "Welcome!")?; + /// + /// let mut child = Router::new(); + /// child.insert("/users/{id}", "A User")?; + /// + /// root.merge(child)?; + /// assert!(root.at("/users/1").is_ok()); + /// # Ok(()) + /// # } + /// ``` + pub fn merge(&mut self, other: Self) -> Result<(), MergeError> { + let mut errors = vec![]; + other.root.for_each(|path, value| { + if let Err(err) = self.insert(path, value) { + errors.push(err); + } + true + }); + if errors.is_empty() { + Ok(()) + } else { + Err(MergeError(errors)) + } + } } /// A successful match consisting of the registered value diff --git a/src/tree.rs b/src/tree.rs index 3965ac8..3aa2027 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -3,6 +3,7 @@ use crate::{InsertError, MatchError, Params}; use std::cell::UnsafeCell; use std::cmp::min; +use std::collections::VecDeque; use std::ops::Range; use std::{fmt, mem}; @@ -660,6 +661,28 @@ impl Node { } } +impl Node { + /// Iterates over the tree and calls the given visitor function + /// with fully resolved path and its value. + pub fn for_each bool>(self, mut visitor: V) { + let mut queue = VecDeque::from([(self.prefix.clone(), self)]); + while let Some((mut prefix, mut node)) = queue.pop_front() { + denormalize_params(&mut prefix, &node.remapping); + if let Some(value) = node.value.take() { + let path = String::from_utf8(prefix.unescaped().to_vec()).unwrap(); + if !visitor(path, value.into_inner()) { + return; + } + } + for child in node.children { + let mut prefix = prefix.clone(); + prefix.append(&child.prefix); + queue.push_back((prefix, child)); + } + } + } +} + /// An ordered list of route parameters keys for a specific route. /// /// To support conflicting routes like `/{a}/foo` and `/{b}/bar`, route parameters diff --git a/tests/merge.rs b/tests/merge.rs new file mode 100644 index 0000000..a4fa2bb --- /dev/null +++ b/tests/merge.rs @@ -0,0 +1,65 @@ +use matchit::{InsertError, Router}; + +#[test] +fn merge_ok() { + let mut root = Router::new(); + assert!(root.insert("/foo", "foo").is_ok()); + assert!(root.insert("/bar/{id}", "bar").is_ok()); + + let mut child = Router::new(); + assert!(child.insert("/baz", "baz").is_ok()); + assert!(child.insert("/xyz/{id}", "xyz").is_ok()); + + assert!(root.merge(child).is_ok()); + + assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); + assert_eq!(root.at("/bar/1").map(|m| *m.value), Ok("bar")); + assert_eq!(root.at("/baz").map(|m| *m.value), Ok("baz")); + assert_eq!(root.at("/xyz/2").map(|m| *m.value), Ok("xyz")); +} + +#[test] +fn merge_conflict() { + let mut root = Router::new(); + assert!(root.insert("/foo", "foo").is_ok()); + assert!(root.insert("/bar", "bar").is_ok()); + + let mut child = Router::new(); + assert!(child.insert("/foo", "changed").is_ok()); + assert!(child.insert("/bar", "changed").is_ok()); + assert!(child.insert("/baz", "baz").is_ok()); + + let errors = root.merge(child).unwrap_err(); + + assert_eq!( + errors.get(0), + Some(&InsertError::Conflict { + with: "/foo".into() + }) + ); + + assert_eq!( + errors.get(1), + Some(&InsertError::Conflict { + with: "/bar".into() + }) + ); + + assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); + assert_eq!(root.at("/bar").map(|m| *m.value), Ok("bar")); + assert_eq!(root.at("/baz").map(|m| *m.value), Ok("baz")); +} + +#[test] +fn merge_nested() { + let mut root = Router::new(); + assert!(root.insert("/foo", "foo").is_ok()); + + let mut child = Router::new(); + assert!(child.insert("/foo/bar", "bar").is_ok()); + + assert!(root.merge(child).is_ok()); + + assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); + assert_eq!(root.at("/foo/bar").map(|m| *m.value), Ok("bar")); +}