From 416a18abf8960a135f9968da8612762eeb65533f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 2 Apr 2016 01:46:41 -0700 Subject: [PATCH] Added ranked routing. --- lib/src/router/collider.rs | 126 ++++++++++++------------------------- lib/src/router/mod.rs | 75 ++++++++++++++++++---- lib/src/router/route.rs | 17 ++++- 3 files changed, 119 insertions(+), 99 deletions(-) diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index e2f92d46..75a2ea24 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -1,6 +1,3 @@ -use std::path::Component; -use std::path::Path; - pub trait Collider { 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() } -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 for &'a str { 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)) } - fn collide(a: &'static str, b: &'static str) -> bool { - let route_a = Route::new(Get, a.to_string(), dummy_handler); - route_a.collides_with(&Route::new(Get, b.to_string(), dummy_handler)) + fn unranked_collide(a: &'static str, b: &'static str) -> bool { + let route_a = Route::ranked(0, Get, a.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 { @@ -122,57 +76,57 @@ mod tests { #[test] fn simple_collisions() { - assert!(collide("a", "a")); - assert!(collide("/a", "/a")); - assert!(collide("/hello", "/hello")); - assert!(collide("/hello", "/hello/")); - assert!(collide("/hello/there/how/ar", "/hello/there/how/ar")); - assert!(collide("/hello/there", "/hello/there/")); + assert!(unranked_collide("a", "a")); + assert!(unranked_collide("/a", "/a")); + assert!(unranked_collide("/hello", "/hello")); + assert!(unranked_collide("/hello", "/hello/")); + assert!(unranked_collide("/hello/there/how/ar", "/hello/there/how/ar")); + assert!(unranked_collide("/hello/there", "/hello/there/")); } #[test] fn simple_param_collisions() { - assert!(collide("/hello/", "/hello/")); - assert!(collide("/hello//hi", "/hello//hi")); - assert!(collide("/hello//hi/there", "/hello//hi/there")); - assert!(collide("//hi/there", "//hi/there")); - assert!(collide("//hi/there", "/dude//there")); - assert!(collide("///", "///")); - assert!(collide("////", "////")); + assert!(unranked_collide("/hello/", "/hello/")); + assert!(unranked_collide("/hello//hi", "/hello//hi")); + assert!(unranked_collide("/hello//hi/there", "/hello//hi/there")); + assert!(unranked_collide("//hi/there", "//hi/there")); + assert!(unranked_collide("//hi/there", "/dude//there")); + assert!(unranked_collide("///", "///")); + assert!(unranked_collide("////", "////")); } #[test] fn medium_param_collisions() { - assert!(collide("/hello/", "/hello/bob")); - assert!(collide("/", "//bob")); + assert!(unranked_collide("/hello/", "/hello/bob")); + assert!(unranked_collide("/", "//bob")); } #[test] fn hard_param_collisions() { - assert!(collide("/bob", "/b")); - assert!(collide("/ac", "/abc")); - assert!(collide("/ac", "/azooc")); - assert!(collide("/a", "/a")); - assert!(collide("/", "/a")); - assert!(collide("//", "/a/b")); - assert!(collide("//bc", "/a/b")); - assert!(collide("//bcd", "/a/b")); + assert!(unranked_collide("/bob", "/b")); + assert!(unranked_collide("/ac", "/abc")); + assert!(unranked_collide("/ac", "/azooc")); + assert!(unranked_collide("/a", "/a")); + assert!(unranked_collide("/", "/a")); + assert!(unranked_collide("//", "/a/b")); + assert!(unranked_collide("//bc", "/a/b")); + assert!(unranked_collide("//bcd", "/a/b")); } #[test] fn non_collisions() { - assert!(!collide("/a", "/b")); - assert!(!collide("/a/b", "/a")); - assert!(!collide("/a/b", "/a/c")); - assert!(!collide("/a/hello", "/a/c")); - assert!(!collide("/hello", "/a/c")); - assert!(!collide("/hello/there", "/hello/there/guy")); - assert!(!collide("/b/there", "/hi/there")); - assert!(!collide("//c", "/hi/person")); - assert!(!collide("//cd", "/hi/e")); - assert!(!collide("/a/", "/b/")); - assert!(!collide("/a/", "/b/")); - assert!(!collide("/a/", "/b/")); + assert!(!unranked_collide("/a", "/b")); + assert!(!unranked_collide("/a/b", "/a")); + assert!(!unranked_collide("/a/b", "/a/c")); + assert!(!unranked_collide("/a/hello", "/a/c")); + assert!(!unranked_collide("/hello", "/a/c")); + assert!(!unranked_collide("/hello/there", "/hello/there/guy")); + assert!(!unranked_collide("/b/there", "/hi/there")); + assert!(!unranked_collide("//c", "/hi/person")); + assert!(!unranked_collide("//cd", "/hi/e")); + assert!(!unranked_collide("/a/", "/b/")); + assert!(!unranked_collide("/a/", "/b/")); + assert!(!unranked_collide("/a/", "/b/")); } #[test] diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index e02891e8..2dc01197 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -19,9 +19,7 @@ pub struct Router { impl Router { pub fn new() -> Router { - Router { - routes: HashMap::new() - } + Router { routes: HashMap::new() } } pub fn add(&mut self, route: Route) { @@ -33,14 +31,18 @@ impl Router { // struct to something like `RocketRouter`. If that happens, returning a // `Route` structure is inflexible. Have it be an associated type. 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 num_segments = path.segment_count(); if let Some(routes) = self.routes.get(&(method, num_segments)) { for route in routes.iter().filter(|r| r.collides_with(uri)) { 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); } } @@ -89,19 +91,47 @@ mod test { 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] fn test_collisions() { - let router = router_with_routes(&["/hello", "/hello"]); + let router = router_with_unranked_routes(&["/hello", "/hello"]); assert!(router.has_collisions()); + let router = router_with_unranked_routes(&["/", "/hello"]); + assert!(router.has_collisions()); + + let router = router_with_unranked_routes(&["/", "/"]); + assert!(router.has_collisions()); + + let router = router_with_unranked_routes(&["/hello/bob", "/hello/"]); + assert!(router.has_collisions()); + + let router = router_with_routes(&["/a/b//d", "///c/d"]); + assert!(router.has_collisions()); + } + + #[test] + fn test_none_collisions_when_ranked() { let router = router_with_routes(&["/", "/hello"]); - assert!(router.has_collisions()); - - let router = router_with_routes(&["/", "/"]); - assert!(router.has_collisions()); + assert!(!router.has_collisions()); let router = router_with_routes(&["/hello/bob", "/hello/"]); - assert!(router.has_collisions()); + assert!(!router.has_collisions()); + + let router = router_with_routes(&["/a/b/c/d", "///c/d"]); + assert!(!router.has_collisions()); + + let router = router_with_routes(&["/hi", "/"]); + assert!(!router.has_collisions()); } #[test] @@ -159,6 +189,29 @@ mod test { 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", "/"], "/hello", "/hello"); + assert_ranked_routes!(&["/", "/hello"], "/hello", "/hello"); + assert_ranked_routes!(&["/", "/hi", "/"], "/hi", "/hi"); + assert_ranked_routes!(&["//b", "/hi/c"], "/hi/c", "/hi/c"); + assert_ranked_routes!(&["//", "/hi/a"], "/hi/c", "//"); + assert_ranked_routes!(&["/hi/a", "/hi/"], "/hi/c", "/hi/"); + } + + #[test] + fn test_ranking() { + // FIXME: Add tests for non-default ranks. + } + fn match_params(router: &Router, path: &str, expected: &[&str]) -> bool { router.route(Get, path).map_or(false, |route| { let params = route.get_params(path); diff --git a/lib/src/router/route.rs b/lib/src/router/route.rs index dcf39915..5423219d 100644 --- a/lib/src/router/route.rs +++ b/lib/src/router/route.rs @@ -11,14 +11,26 @@ pub struct Route { pub method: Method, pub handler: Handler<'static>, pub path: URIBuf, + pub rank: isize } 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 { method: m, path: URIBuf::new(path), 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 { fn collides_with(&self, b: &Route) -> bool { if self.path.segment_count() != b.path.segment_count() - || self.method != b.method { + || self.method != b.method + || self.rank != b.rank { return false; }