Skip to content

Commit

Permalink
refactor and document internals
Browse files Browse the repository at this point in the history
  • Loading branch information
ibraheemdev committed Jul 11, 2024
1 parent 4b9e45c commit 9937b49
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 318 deletions.
43 changes: 25 additions & 18 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ impl fmt::Display for InsertError {
Self::Conflict { with } => {
write!(

Check warning on line 32 in src/error.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

variables can be used directly in the `format!` string
f,
"insertion failed due to conflict with previously registered route: {}",
"Insertion failed due to conflict with previously registered route: {}",
with
)
}
Self::InvalidParamSegment => {
write!(f, "only one parameter is allowed per path segment")
write!(f, "Only one parameter is allowed per path segment")
}
Self::InvalidParam => write!(f, "parameters must be registered with a valid name"),
Self::InvalidParam => write!(f, "Parameters must be registered with a valid name"),
Self::InvalidCatchAll => write!(
f,
"catch-all parameters are only allowed at the end of a route"
"Catch-all parameters are only allowed at the end of a route"
),
}
}
Expand All @@ -50,42 +50,49 @@ impl fmt::Display for InsertError {
impl std::error::Error for InsertError {}

impl InsertError {
/// Returns an error for a route conflict with the given node.
///
/// This method attempts to find the full conflicting route.
pub(crate) fn conflict<T>(
route: &UnescapedRoute,
prefix: UnescapedRef<'_>,
current: &Node<T>,
) -> Self {
let mut route = route.clone();

// The new route would have had to replace the current node in the tree.
if prefix.inner() == current.prefix.inner() {
denormalize_params(&mut route, &current.param_remapping);
// The route is conflicting with the current node.
if prefix.unescaped() == current.prefix.unescaped() {
denormalize_params(&mut route, &current.remapping);
return InsertError::Conflict {

Check warning on line 66 in src/error.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

unnecessary structure name repetition
with: String::from_utf8(route.into_inner()).unwrap(),
with: String::from_utf8(route.into_unescaped()).unwrap(),
};
}

// Remove the non-matching suffix from the route.
route.truncate(route.len() - prefix.len());

// Add the conflicting prefix.
if !route.ends_with(&current.prefix) {
route.append(&current.prefix);
}

// Add the prefixes of any conflicting children.
let mut child = current.children.first();
while let Some(node) = child {
route.append(&node.prefix);
child = node.children.first();
}

// Denormalize any route parameters.
let mut last = current;
while let Some(node) = last.children.first() {
last = node;
}
denormalize_params(&mut route, &last.remapping);

let mut current = current.children.first();
while let Some(node) = current {
route.append(&node.prefix);
current = node.children.first();
}

denormalize_params(&mut route, &last.param_remapping);

// Return the conflicting route.
InsertError::Conflict {

Check warning on line 94 in src/error.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

unnecessary structure name repetition
with: String::from_utf8(route.into_inner()).unwrap(),
with: String::from_utf8(route.into_unescaped()).unwrap(),
}
}
}
Expand Down Expand Up @@ -113,7 +120,7 @@ pub enum MatchError {

impl fmt::Display for MatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "matching route not found")
write!(f, "Matching route not found")
}
}

Expand Down
21 changes: 11 additions & 10 deletions src/escape.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::{fmt, ops::Range};

/// An uescaped route that keeps track of the position of escaped characters ('{{' or '}}').
/// An unescaped route that keeps track of the position of escaped characters ('{{' or '}}').
///
/// Note that this type dereferences to `&[u8]`.
#[derive(Clone, Default)]
pub struct UnescapedRoute {
// The raw unescaped route.
inner: Vec<u8>,
escaped: Vec<usize>,
}
Expand Down Expand Up @@ -40,10 +41,10 @@ impl UnescapedRoute {
range: Range<usize>,
replace: Vec<u8>,
) -> impl Iterator<Item = u8> + '_ {
// ignore any escaped characters in the range being replaced
// Ignore any escaped characters in the range being replaced.
self.escaped.retain(|x| !range.contains(x));

// update the escaped indices
// Update the escaped indices.
let offset = (replace.len() as isize) - (range.len() as isize);

Check warning on line 48 in src/escape.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

casting `usize` to `isize` may wrap around the value

Check warning on line 48 in src/escape.rs

View workflow job for this annotation

GitHub Actions / Clippy Lints

casting `usize` to `isize` may wrap around the value
for i in &mut self.escaped {
if *i > range.end {
Expand Down Expand Up @@ -78,13 +79,13 @@ impl UnescapedRoute {
}
}

/// Returns a reference to the inner slice.
pub fn inner(&self) -> &[u8] {
/// Returns a reference to the unescaped slice.
pub fn unescaped(&self) -> &[u8] {
&self.inner
}

/// Returns the inner slice.
pub fn into_inner(self) -> Vec<u8> {
/// Returns the unescaped route.
pub fn into_unescaped(self) -> Vec<u8> {
self.inner
}
}
Expand Down Expand Up @@ -131,7 +132,7 @@ impl<'a> UnescapedRef<'a> {
}
}

/// Returns true if the character at the given index was escaped.
/// Returns `true` if the character at the given index was escaped.
pub fn is_escaped(&self, i: usize) -> bool {
if let Some(i) = i.checked_add_signed(-self.offset) {
return self.escaped.contains(&i);
Expand All @@ -158,8 +159,8 @@ impl<'a> UnescapedRef<'a> {
}
}

/// Returns a reference to the inner slice.
pub fn inner(&self) -> &[u8] {
/// Returns a reference to the unescaped slice.
pub fn unescaped(&self) -> &[u8] {
self.inner
}
}
Expand Down
18 changes: 13 additions & 5 deletions src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::{fmt, iter, mem, slice};
/// A single URL parameter, consisting of a key and a value.
#[derive(PartialEq, Eq, Ord, PartialOrd, Default, Copy, Clone)]
struct Param<'k, 'v> {
// Keys and values are stored as byte slices internally by the router
// to avoid UTF8 checks when slicing, but UTF8 is still respected,
// so these slices are valid strings.
key: &'k [u8],
value: &'v [u8],
}
Expand All @@ -13,11 +16,12 @@ impl<'k, 'v> Param<'k, 'v> {
value: b"",
};

// this could be from_utf8_unchecked, but we'll keep this safe for now
// Returns the parameter key as a string.
fn key_str(&self) -> &'k str {
std::str::from_utf8(self.key).unwrap()
}

// Returns the parameter value as a string.
fn value_str(&self) -> &'v str {
std::str::from_utf8(self.value).unwrap()
}
Expand All @@ -31,12 +35,12 @@ impl<'k, 'v> Param<'k, 'v> {
/// # router.insert("/users/{id}", true).unwrap();
/// let matched = router.at("/users/1")?;
///
/// // you can iterate through the keys and values
/// // Iterate through the keys and values.
/// for (key, value) in matched.params.iter() {
/// println!("key: {}, value: {}", key, value);
/// }
///
/// // or get a specific value by key
/// // Get a specific value by name.
/// let id = matched.params.get("id");
/// assert_eq!(id, Some("1"));
/// # Ok(())
Expand All @@ -47,9 +51,11 @@ pub struct Params<'k, 'v> {
kind: ParamsKind<'k, 'v>,
}

// most routes have 1-3 dynamic parameters, so we can avoid a heap allocation in common cases.
// Most routes have a small number of dynamic parameters, so we can avoid
// heap allocations in the common case.
const SMALL: usize = 3;

// A list of parameters, optimized to avoid allocations when possible.
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone)]
enum ParamsKind<'k, 'v> {
Small([Param<'k, 'v>; SMALL], usize),
Expand All @@ -71,6 +77,7 @@ impl<'k, 'v> Params<'k, 'v> {
}
}

// Truncates the parameter list to the given length.
pub(crate) fn truncate(&mut self, n: usize) {
match &mut self.kind {
ParamsKind::Small(_, len) => *len = n,
Expand Down Expand Up @@ -133,7 +140,7 @@ impl<'k, 'v> Params<'k, 'v> {
}
}

// Transform each key.
// Applies a transformation function to each key.
pub(crate) fn for_each_key_mut(&mut self, f: impl Fn((usize, &mut &'k [u8]))) {
match &mut self.kind {
ParamsKind::Small(arr, len) => arr
Expand Down Expand Up @@ -191,6 +198,7 @@ impl<'ps, 'k, 'v> Iterator for ParamsIter<'ps, 'k, 'v> {
}
}
}

impl ExactSizeIterator for ParamsIter<'_, '_, '_> {
fn len(&self) -> usize {
match self.kind {
Expand Down
14 changes: 8 additions & 6 deletions src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl<T> Router<T> {
Self::default()
}

/// Insert a route.
/// Insert a route into the router.
///
/// # Examples
///
Expand All @@ -37,7 +37,7 @@ impl<T> Router<T> {
/// # }
/// ```
pub fn insert(&mut self, route: impl Into<String>, value: T) -> Result<(), InsertError> {
self.root.insert(route, value)
self.root.insert(route.into(), value)
}

/// Tries to find a value in the router matching the given path.
Expand All @@ -58,7 +58,7 @@ impl<T> Router<T> {
pub fn at<'m, 'p>(&'m self, path: &'p str) -> Result<Match<'m, 'p, &'m T>, MatchError> {
match self.root.at(path.as_bytes()) {
Ok((value, params)) => Ok(Match {
// SAFETY: We only expose &mut T through &mut self
// Safety: We only expose `&mut T` through `&mut self`
value: unsafe { &*value.get() },
params,
}),
Expand Down Expand Up @@ -88,7 +88,7 @@ impl<T> Router<T> {
) -> Result<Match<'m, 'p, &'m mut T>, MatchError> {
match self.root.at(path.as_bytes()) {
Ok((value, params)) => Ok(Match {
// SAFETY: We have &mut self
// Safety: We have `&mut self`
value: unsafe { &mut *value.get() },
params,
}),
Expand All @@ -98,7 +98,8 @@ impl<T> Router<T> {

/// Remove a given route from the router.
///
/// Returns the value stored under the route if it was found. If the route was not found or invalid, `None` is returned.
/// Returns the value stored under the route if it was found.
/// If the route was not found or invalid, `None` is returned.
///
/// # Examples
///
Expand All @@ -125,7 +126,7 @@ impl<T> Router<T> {
/// assert_eq!(router.remove("/home/{id}/"), Some("Hello!"));
/// ```
pub fn remove(&mut self, path: impl Into<String>) -> Option<T> {
self.root.remove(path)
self.root.remove(path.into())
}

#[cfg(feature = "__test_helpers")]
Expand All @@ -140,6 +141,7 @@ impl<T> Router<T> {
pub struct Match<'k, 'v, V> {
/// The value stored under the matched node.
pub value: V,

/// The route parameters. See [parameters](crate#parameters) for more details.
pub params: Params<'k, 'v>,
}
Loading

0 comments on commit 9937b49

Please sign in to comment.