Skip to content

Commit

Permalink
add Router::merge
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
darkenmay authored Oct 14, 2024
1 parent 8913972 commit 8e66ae9
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -97,6 +98,29 @@ impl InsertError {
}
}

/// A failed merge attempt.
#[derive(Debug, Clone)]
pub struct MergeError(pub(crate) Vec<InsertError>);

Check warning on line 103 in src/error.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

item name ends with its containing module's name

impl fmt::Display for MergeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for error in self.0.iter() {

Check warning on line 107 in src/error.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

it is more concise to loop over references to containers instead of using explicit iteration methods
writeln!(f, "{}", error)?;

Check warning on line 108 in src/error.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

variables can be used directly in the `format!` string
}
Ok(())
}
}

impl std::error::Error for MergeError {}

impl Deref for MergeError {
type Target = Vec<InsertError>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

/// A failed match attempt.
///
/// ```
Expand Down
34 changes: 34 additions & 0 deletions src/router.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::error::MergeError;
use crate::tree::Node;
use crate::{InsertError, MatchError, Params};

Expand Down Expand Up @@ -133,6 +134,39 @@ impl<T> Router<T> {
pub fn check_priorities(&self) -> Result<u32, (u32, u32)> {
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<dyn std::error::Error>> {
/// 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
Expand Down
23 changes: 23 additions & 0 deletions src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -660,6 +661,28 @@ impl<T> Node<T> {
}
}

impl<T> Node<T> {
/// Iterates over the tree and calls the given visitor function
/// with fully resolved path and its value.
pub fn for_each<V: FnMut(String, T) -> 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
Expand Down
65 changes: 65 additions & 0 deletions tests/merge.rs
Original file line number Diff line number Diff line change
@@ -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"));
}

0 comments on commit 8e66ae9

Please sign in to comment.