Skip to content

Commit

Permalink
several changes for 0.4.0:
Browse files Browse the repository at this point in the history
* add support for dots
* slight perf improvement through use of smartstring
* removes reverse match, replaces it with Route::template
  • Loading branch information
jbr committed Aug 11, 2021
1 parent 7716ab4 commit 827640a
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 212 deletions.
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "routefinder"
version = "0.3.1"
version = "0.4.0"
authors = ["Jacob Rothstein <hi@jbr.me>"]
edition = "2018"
description = "router"
Expand All @@ -11,8 +11,11 @@ license = "MIT OR Apache-2.0"
keywords = ["router"]
categories = ["web-programming::http-server", "web-programming"]

[dependencies]
smartstring = "0.2.9"

[dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] }
criterion = { version = "0.3.5", features = ["html_reports"] }

[[bench]]
name = "bench"
Expand Down
18 changes: 12 additions & 6 deletions benches/bench.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{black_box, criterion_group, criterion_main, Criterion};

use routefinder::*;

Expand All @@ -20,18 +20,24 @@ fn benchmark(c: &mut Criterion) {
b.iter(|| router.best_match("/posts/100/comments"))
});

c.bench_function("/posts/n", |b| b.iter(|| router.best_match("/posts/100")));
c.bench_function("/posts/n", |b| {
b.iter(|| router.best_match(black_box("/posts/100")))
});

c.bench_function("/posts", |b| b.iter(|| router.best_match("/posts")));
c.bench_function("/posts", |b| {
b.iter(|| router.best_match(black_box("/posts")))
});

c.bench_function("/comments", |b| b.iter(|| router.best_match("/comments")));
c.bench_function("/comments", |b| {
b.iter(|| router.best_match(black_box("/comments")))
});

c.bench_function("/comments/n", |b| {
b.iter(|| router.best_match("/comments/100"))
b.iter(|| router.best_match(black_box("/comments/100")))
});

c.bench_function("fallthrough", |b| {
b.iter(|| router.best_match("/a/b/c/d/e/f"))
b.iter(|| router.best_match(black_box("/a/b/c/d/e/f")))
});
}

Expand Down
22 changes: 22 additions & 0 deletions src/captures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ impl<'key, 'value> From<(&'key str, &'value str)> for Capture<'key, 'value> {
}
}

impl<'pair, 'key: 'pair, 'value: 'pair> From<&'pair (&'key str, &'value str)>
for Capture<'key, 'value>
{
fn from(kv: &'pair (&'key str, &'value str)) -> Self {
Self {
key: kv.0.into(),
value: kv.1.into(),
}
}
}

impl<'keys, 'values, F> From<F> for Captures<'keys, 'values>
where
F: IntoIterator<Item = (&'keys str, &'values str)>,
Expand All @@ -141,6 +152,17 @@ impl<'keys, 'values> FromIterator<(&'keys str, &'values str)> for Captures<'keys
}
}

impl<'pair, 'keys: 'pair, 'values: 'pair> FromIterator<&'pair (&'keys str, &'values str)>
for Captures<'keys, 'values>
{
fn from_iter<T: IntoIterator<Item = &'pair (&'keys str, &'values str)>>(iter: T) -> Self {
Self {
params: iter.into_iter().map(Into::into).collect(),
wildcard: None,
}
}
}

impl<'keys, 'values> Extend<(&'keys str, &'values str)> for Captures<'keys, 'values> {
fn extend<T: IntoIterator<Item = (&'keys str, &'values str)>>(&mut self, iter: T) {
for (k, v) in iter {
Expand Down
12 changes: 5 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#![forbid(unsafe_code, future_incompatible)]
#![forbid(unsafe_code)]
#![deny(
clippy::dbg_macro,
missing_copy_implementations,
rustdoc::missing_crate_level_docs,
missing_debug_implementations,
nonstandard_style,
missing_copy_implementations,
unused_qualifications
)]
#![warn(missing_docs)]

//! # Routefinder
//!
Expand All @@ -27,11 +30,6 @@
//! assert_eq!(router.matches("/hello").len(), 3);
//! assert_eq!(router.matches("/").len(), 1);
//!
//! // reverse lookup
//! let captures = Captures::from(vec![("world", "jupiter")]);
//! let reverse_match = router.best_reverse_match(&captures).unwrap();
//! assert_eq!(*reverse_match, 4);
//! assert_eq!(reverse_match.to_string(), "/hey/jupiter");
//! # Ok(()) }
//! ```
//!
Expand Down
84 changes: 5 additions & 79 deletions src/match.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::{cmp::Ordering, ops::Deref};

use crate::{Capture, Captures, Route, RouteSpec, Segment};
use std::{cmp::Ordering, ops::Deref};

/// The output of a successful application of a
/// [`Route`] to a str path, as well as references to any captures.
Expand All @@ -9,85 +8,12 @@ use crate::{Capture, Captures, Route, RouteSpec, Segment};
#[derive(Debug)]
pub struct Match<'router, 'path, T> {
path: &'path str,
route: &'router Route<T>,
captures: Vec<&'path str>,
pub(crate) path: &'path str,
pub(crate) route: &'router Route<T>,
pub(crate) captures: Vec<&'path str>,
}

impl<'router, 'path, T> Match<'router, 'path, T> {
/// Attempts to build a new Match from the provided path string
/// and route. Returns None if the match was unsuccessful
pub fn new(path: &'path str, route: &'router Route<T>) -> Option<Self> {
let mut p = path.trim_start_matches('/').trim_end_matches('/');
let mut captures = vec![];

let mut peek = route.segments().iter().peekable();
while let Some(segment) = peek.next() {
p = match &*segment {
Segment::Exact(e) => {
if p.starts_with(&*e) {
&p[e.len()..]
} else {
return None;
}
}

Segment::Param(_) => {
if p.is_empty() { return None; }
match peek.peek() {
None | Some(Segment::Slash) => {
let capture = p.split('/').next()?;
captures.push(capture);
&p[capture.len()..]
}
Some(Segment::Dot) => {
let index = p.find(|c| c == '.' || c == '/')?;
if p.chars().nth(index) == Some('.') {
captures.push(&p[..index]);
&p[index + 1..]
} else {
return None;
}
}
_ => panic!("param must be followed by a dot, a slash, or the end of the route"),
}
}

Segment::Wildcard => {
match peek.peek() {
Some(_) => panic!("wildcard must currently be the terminal segment, please file an issue if you have a use case for a mid-route *"),
None => {
captures.push(p);
""
}
}
}

Segment::Slash => match (p.chars().next(), peek.peek()) {
(Some('/'),Some(_)) => &p[1..],
(None, None) => p,
(None, Some(Segment::Wildcard)) => p,
_ => return None,
}

Segment::Dot => match p.chars().next() {
Some('.') => &p[1..],
_ => return None,
}
}
}

if p.is_empty() || p == "/" {
Some(Self {
path,
route,
captures,
})
} else {
None
}
}

/// Returns a reference to the handler associated with this route
pub fn handler(&self) -> &'router T {
self.route.handler()
Expand All @@ -109,7 +35,7 @@ impl<'router, 'path, T> Match<'router, 'path, T> {
Captures::default(),
|mut captures, (segment, capture)| match segment {
Segment::Param(name) => {
captures.push(Capture::new(name, *capture));
captures.push(Capture::new(&**name, *capture));
captures
}

Expand Down
Loading

0 comments on commit 827640a

Please sign in to comment.