Added ranked routing.

This commit is contained in:
Sergio Benitez 2016-04-02 01:46:41 -07:00
parent 69a19ccc7f
commit 416a18abf8
3 changed files with 119 additions and 99 deletions

View File

@ -1,6 +1,3 @@
use std::path::Component;
use std::path::Path;
pub trait Collider<T: ?Sized = Self> { pub trait Collider<T: ?Sized = Self> {
fn collides_with(&self, other: &T) -> bool; fn collides_with(&self, other: &T) -> bool;
} }
@ -33,53 +30,10 @@ fn do_match_until(break_c: char, a: &str, b: &str, dir: bool) -> bool {
index_match_until(break_c, a, b, dir).is_some() index_match_until(break_c, a, b, dir).is_some()
} }
macro_rules! comp_to_str {
($component:expr) => (
match $component {
&Component::Normal(ref comp) => {
if let Some(string) = comp.to_str() { string }
else { return true }
},
_ => return true
};
)
}
impl Collider for Path {
// TODO: It's expensive to compute the number of components: O(n) per path
// where n == number of chars.
//
// Idea: Create a `CachedPath` type that caches the number of components
// similar to the way `Route` does it.
fn collides_with(&self, b: &Path) -> bool {
if self.components().count() != b.components().count() {
return false;
}
let mut a_components = self.components();
let mut b_components = b.components();
while let Some(ref c1) = a_components.next() {
if let Some(ref c2) = b_components.next() {
if !c1.collides_with(c2) {
return false
}
}
}
true
}
}
impl<'a> Collider for Component<'a> {
fn collides_with(&self, other: &Component<'a>) -> bool {
let (a, b) = (comp_to_str!(self), comp_to_str!(other));
do_match_until('<', a, b, true) && do_match_until('>', a, b, false)
}
}
impl<'a> Collider<str> for &'a str { impl<'a> Collider<str> for &'a str {
fn collides_with(&self, other: &str) -> bool { fn collides_with(&self, other: &str) -> bool {
Path::new(self).collides_with(Path::new(other)) let (a, b) = (self, other);
do_match_until('<', a, b, true) && do_match_until('>', a, b, false)
} }
} }
@ -102,9 +56,9 @@ mod tests {
route_a.collides_with(&Route::new(b.0, b.1.to_string(), dummy_handler)) route_a.collides_with(&Route::new(b.0, b.1.to_string(), dummy_handler))
} }
fn collide(a: &'static str, b: &'static str) -> bool { fn unranked_collide(a: &'static str, b: &'static str) -> bool {
let route_a = Route::new(Get, a.to_string(), dummy_handler); let route_a = Route::ranked(0, Get, a.to_string(), dummy_handler);
route_a.collides_with(&Route::new(Get, b.to_string(), dummy_handler)) route_a.collides_with(&Route::ranked(0, Get, b.to_string(), dummy_handler))
} }
fn s_r_collide(a: &'static str, b: &'static str) -> bool { fn s_r_collide(a: &'static str, b: &'static str) -> bool {
@ -122,57 +76,57 @@ mod tests {
#[test] #[test]
fn simple_collisions() { fn simple_collisions() {
assert!(collide("a", "a")); assert!(unranked_collide("a", "a"));
assert!(collide("/a", "/a")); assert!(unranked_collide("/a", "/a"));
assert!(collide("/hello", "/hello")); assert!(unranked_collide("/hello", "/hello"));
assert!(collide("/hello", "/hello/")); assert!(unranked_collide("/hello", "/hello/"));
assert!(collide("/hello/there/how/ar", "/hello/there/how/ar")); assert!(unranked_collide("/hello/there/how/ar", "/hello/there/how/ar"));
assert!(collide("/hello/there", "/hello/there/")); assert!(unranked_collide("/hello/there", "/hello/there/"));
} }
#[test] #[test]
fn simple_param_collisions() { fn simple_param_collisions() {
assert!(collide("/hello/<name>", "/hello/<person>")); assert!(unranked_collide("/hello/<name>", "/hello/<person>"));
assert!(collide("/hello/<name>/hi", "/hello/<person>/hi")); assert!(unranked_collide("/hello/<name>/hi", "/hello/<person>/hi"));
assert!(collide("/hello/<name>/hi/there", "/hello/<person>/hi/there")); assert!(unranked_collide("/hello/<name>/hi/there", "/hello/<person>/hi/there"));
assert!(collide("/<name>/hi/there", "/<person>/hi/there")); assert!(unranked_collide("/<name>/hi/there", "/<person>/hi/there"));
assert!(collide("/<name>/hi/there", "/dude/<name>/there")); assert!(unranked_collide("/<name>/hi/there", "/dude/<name>/there"));
assert!(collide("/<name>/<a>/<b>", "/<a>/<b>/<c>")); assert!(unranked_collide("/<name>/<a>/<b>", "/<a>/<b>/<c>"));
assert!(collide("/<name>/<a>/<b>/", "/<a>/<b>/<c>/")); assert!(unranked_collide("/<name>/<a>/<b>/", "/<a>/<b>/<c>/"));
} }
#[test] #[test]
fn medium_param_collisions() { fn medium_param_collisions() {
assert!(collide("/hello/<name>", "/hello/bob")); assert!(unranked_collide("/hello/<name>", "/hello/bob"));
assert!(collide("/<name>", "//bob")); assert!(unranked_collide("/<name>", "//bob"));
} }
#[test] #[test]
fn hard_param_collisions() { fn hard_param_collisions() {
assert!(collide("/<name>bob", "/<name>b")); assert!(unranked_collide("/<name>bob", "/<name>b"));
assert!(collide("/a<b>c", "/abc")); assert!(unranked_collide("/a<b>c", "/abc"));
assert!(collide("/a<b>c", "/azooc")); assert!(unranked_collide("/a<b>c", "/azooc"));
assert!(collide("/a<b>", "/a")); assert!(unranked_collide("/a<b>", "/a"));
assert!(collide("/<b>", "/a")); assert!(unranked_collide("/<b>", "/a"));
assert!(collide("/<a>/<b>", "/a/b<c>")); assert!(unranked_collide("/<a>/<b>", "/a/b<c>"));
assert!(collide("/<a>/bc<b>", "/a/b<c>")); assert!(unranked_collide("/<a>/bc<b>", "/a/b<c>"));
assert!(collide("/<a>/bc<b>d", "/a/b<c>")); assert!(unranked_collide("/<a>/bc<b>d", "/a/b<c>"));
} }
#[test] #[test]
fn non_collisions() { fn non_collisions() {
assert!(!collide("/a", "/b")); assert!(!unranked_collide("/a", "/b"));
assert!(!collide("/a/b", "/a")); assert!(!unranked_collide("/a/b", "/a"));
assert!(!collide("/a/b", "/a/c")); assert!(!unranked_collide("/a/b", "/a/c"));
assert!(!collide("/a/hello", "/a/c")); assert!(!unranked_collide("/a/hello", "/a/c"));
assert!(!collide("/hello", "/a/c")); assert!(!unranked_collide("/hello", "/a/c"));
assert!(!collide("/hello/there", "/hello/there/guy")); assert!(!unranked_collide("/hello/there", "/hello/there/guy"));
assert!(!collide("/b<a>/there", "/hi/there")); assert!(!unranked_collide("/b<a>/there", "/hi/there"));
assert!(!collide("/<a>/<b>c", "/hi/person")); assert!(!unranked_collide("/<a>/<b>c", "/hi/person"));
assert!(!collide("/<a>/<b>cd", "/hi/<a>e")); assert!(!unranked_collide("/<a>/<b>cd", "/hi/<a>e"));
assert!(!collide("/a<a>/<b>", "/b<b>/<a>")); assert!(!unranked_collide("/a<a>/<b>", "/b<b>/<a>"));
assert!(!collide("/a/<b>", "/b/<b>")); assert!(!unranked_collide("/a/<b>", "/b/<b>"));
assert!(!collide("/a<a>/<b>", "/b/<b>")); assert!(!unranked_collide("/a<a>/<b>", "/b/<b>"));
} }
#[test] #[test]

View File

@ -19,9 +19,7 @@ pub struct Router {
impl Router { impl Router {
pub fn new() -> Router { pub fn new() -> Router {
Router { Router { routes: HashMap::new() }
routes: HashMap::new()
}
} }
pub fn add(&mut self, route: Route) { pub fn add(&mut self, route: Route) {
@ -33,14 +31,18 @@ impl Router {
// struct to something like `RocketRouter`. If that happens, returning a // struct to something like `RocketRouter`. If that happens, returning a
// `Route` structure is inflexible. Have it be an associated type. // `Route` structure is inflexible. Have it be an associated type.
pub fn route<'b>(&'b self, method: Method, uri: &str) -> Option<&'b Route> { pub fn route<'b>(&'b self, method: Method, uri: &str) -> Option<&'b Route> {
let mut matched_route = None; let mut matched_route: Option<&Route> = None;
let path = URI::new(uri); let path = URI::new(uri);
let num_segments = path.segment_count(); let num_segments = path.segment_count();
if let Some(routes) = self.routes.get(&(method, num_segments)) { if let Some(routes) = self.routes.get(&(method, num_segments)) {
for route in routes.iter().filter(|r| r.collides_with(uri)) { for route in routes.iter().filter(|r| r.collides_with(uri)) {
println!("\t=> Matched {} to: {}", uri, route); println!("\t=> Matched {} to: {}", uri, route);
if let None = matched_route { if let Some(existing_route) = matched_route {
if route.rank > existing_route.rank {
matched_route = Some(route);
}
} else {
matched_route = Some(route); matched_route = Some(route);
} }
} }
@ -89,19 +91,47 @@ mod test {
router router
} }
fn router_with_unranked_routes(routes: &[&'static str]) -> Router {
let mut router = Router::new();
for route in routes {
let route = Route::ranked(0, Get, route.to_string(), dummy_handler);
router.add(route);
}
router
}
#[test] #[test]
fn test_collisions() { fn test_collisions() {
let router = router_with_routes(&["/hello", "/hello"]); let router = router_with_unranked_routes(&["/hello", "/hello"]);
assert!(router.has_collisions()); assert!(router.has_collisions());
let router = router_with_unranked_routes(&["/<a>", "/hello"]);
assert!(router.has_collisions());
let router = router_with_unranked_routes(&["/<a>", "/<b>"]);
assert!(router.has_collisions());
let router = router_with_unranked_routes(&["/hello/bob", "/hello/<b>"]);
assert!(router.has_collisions());
let router = router_with_routes(&["/a/b/<c>/d", "/<a>/<b>/c/d"]);
assert!(router.has_collisions());
}
#[test]
fn test_none_collisions_when_ranked() {
let router = router_with_routes(&["/<a>", "/hello"]); let router = router_with_routes(&["/<a>", "/hello"]);
assert!(router.has_collisions()); assert!(!router.has_collisions());
let router = router_with_routes(&["/<a>", "/<b>"]);
assert!(router.has_collisions());
let router = router_with_routes(&["/hello/bob", "/hello/<b>"]); let router = router_with_routes(&["/hello/bob", "/hello/<b>"]);
assert!(router.has_collisions()); assert!(!router.has_collisions());
let router = router_with_routes(&["/a/b/c/d", "/<a>/<b>/c/d"]);
assert!(!router.has_collisions());
let router = router_with_routes(&["/hi", "/<hi>"]);
assert!(!router.has_collisions());
} }
#[test] #[test]
@ -159,6 +189,29 @@ mod test {
assert!(router.route(Put, "/a/b").is_none()); assert!(router.route(Put, "/a/b").is_none());
} }
macro_rules! assert_ranked_routes {
($routes:expr, $to:expr, $want:expr) => ({
let router = router_with_routes($routes);
let route_path = router.route(Get, $to).unwrap().path.as_str();
assert_eq!(route_path as &str, $want as &str);
})
}
#[test]
fn test_default_ranking() {
assert_ranked_routes!(&["/hello", "/<name>"], "/hello", "/hello");
assert_ranked_routes!(&["/<name>", "/hello"], "/hello", "/hello");
assert_ranked_routes!(&["/<a>", "/hi", "/<b>"], "/hi", "/hi");
assert_ranked_routes!(&["/<a>/b", "/hi/c"], "/hi/c", "/hi/c");
assert_ranked_routes!(&["/<a>/<b>", "/hi/a"], "/hi/c", "/<a>/<b>");
assert_ranked_routes!(&["/hi/a", "/hi/<c>"], "/hi/c", "/hi/<c>");
}
#[test]
fn test_ranking() {
// FIXME: Add tests for non-default ranks.
}
fn match_params(router: &Router, path: &str, expected: &[&str]) -> bool { fn match_params(router: &Router, path: &str, expected: &[&str]) -> bool {
router.route(Get, path).map_or(false, |route| { router.route(Get, path).map_or(false, |route| {
let params = route.get_params(path); let params = route.get_params(path);

View File

@ -11,14 +11,26 @@ pub struct Route {
pub method: Method, pub method: Method,
pub handler: Handler<'static>, pub handler: Handler<'static>,
pub path: URIBuf, pub path: URIBuf,
pub rank: isize
} }
impl Route { impl Route {
pub fn new(m: Method, path: String, handler: Handler<'static>) -> Route { pub fn ranked(rank: isize, m: Method, path: String,
handler: Handler<'static>) -> Route {
Route { Route {
method: m, method: m,
path: URIBuf::new(path), path: URIBuf::new(path),
handler: handler, handler: handler,
rank: rank
}
}
pub fn new(m: Method, path: String, handler: Handler<'static>) -> Route {
Route {
method: m,
handler: handler,
rank: (!path.contains("<") as isize),
path: URIBuf::new(path),
} }
} }
@ -53,7 +65,8 @@ impl fmt::Display for Route {
impl Collider for Route { impl Collider for Route {
fn collides_with(&self, b: &Route) -> bool { fn collides_with(&self, b: &Route) -> bool {
if self.path.segment_count() != b.path.segment_count() if self.path.segment_count() != b.path.segment_count()
|| self.method != b.method { || self.method != b.method
|| self.rank != b.rank {
return false; return false;
} }