From 433a9119bdeceb404153c7211fc2b38794bd0bde Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 21 Mar 2016 22:04:39 -0700 Subject: [PATCH] It works! Next steps: clean-up, error handling, docs. --- examples/hello/src/main.rs | 27 ++----- lib/src/error.rs | 1 + lib/src/lib.rs | 29 ++++--- lib/src/request.rs | 31 ++++--- lib/src/router/collider.rs | 30 ++++--- lib/src/router/mod.rs | 148 ++++++++++++++++++++++++++++++++-- lib/src/router/route.rs | 49 +++++++++-- macros/src/route_decorator.rs | 6 +- 8 files changed, 249 insertions(+), 72 deletions(-) diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index 912e003f..c81c84f6 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -3,36 +3,19 @@ extern crate rocket; use rocket::Rocket; - use std::fs::File; #[route(GET, path = "/")] -fn simple() -> File { +fn root() -> File { File::open("/tmp/index.html").unwrap() } -#[route(GET, path = "/hello/")] -fn simple2() -> &'static str { - "Hello, world!" -} - -#[route(GET, path = "/hello")] -fn simple3() -> String { - String::from("Hello, world!") -} - -#[route(GET, path = "//")] -fn simple4(name: &str, age: i8) -> String { +#[route(GET, path = "/hello//")] +fn hello(name: &str, age: i8) -> String { format!("Hello, {} year old named {}!", age, name) } -#[route(GET, path = "/something")] -fn simple5() -> &'static str { - "hi" -} - fn main() { - let mut rocket = Rocket::new("localhost", 8000); - rocket.mount("/", routes![simple, simple2, simple3, simple4, simple5]); - rocket.mount_and_launch("/hello/", routes![simple, simple3, simple4, simple5]); + let rocket = Rocket::new("localhost", 8000); + rocket.mount_and_launch("/", routes![root, hello]); } diff --git a/lib/src/error.rs b/lib/src/error.rs index 8ce28607..b67504b3 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -1,5 +1,6 @@ pub enum Error { BadMethod, BadParse, + NoRoute, NoKey } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 8dd77b0c..cea9d98b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -27,15 +27,14 @@ use hyper::Server; pub type Handler<'a> = fn(Request) -> Response<'a>; -#[allow(dead_code)] -#[derive(Clone)] -pub struct Route<'a> { +// TODO: Figure out if having Handler<'static> there is a good idea. +pub struct Route { pub method: Method, pub path: &'static str, - pub handler: Handler<'a> + pub handler: Handler<'static> } -impl<'a> fmt::Display for Route<'a> { +impl<'a> fmt::Display for Route { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {:?}", Green.paint(&self.method), Blue.paint(&self.path)) } @@ -54,9 +53,15 @@ impl HypHandler for Rocket { if let RequestUri::AbsolutePath(uri_string) = req.uri { if let Some(method) = Method::from_hyp(req.method) { println!("Request: {:?}", uri_string); - self.router.route(method, uri_string.as_str()); - res.send(b"Hello, world!").unwrap(); - return; + let uri_str = uri_string.as_str(); + let route = self.router.route(method, uri_str); + let mut response = route.map_or(Response::not_found(), |route| { + let params = route.get_params(uri_str); + let request = Request::new(params, uri_str); + (route.handler)(request) + }); + + return response.body.respond(res); } } @@ -73,19 +78,17 @@ impl Rocket { } } - pub fn mount(&mut self, base: &'static str, routes: &[&Route<'static>]) - -> &mut Self { + pub fn mount(&mut self, base: &'static str, routes: &[&Route]) -> &mut Self { println!("🛰 {} '{}':", Magenta.paint("Mounting"), Blue.paint(base)); for route in routes { println!("\t* {}", route); - self.router.add_route(route.method.clone(), base, route.path); + self.router.add_route(route.method, base, route.path, route.handler); } self } - pub fn mount_and_launch(mut self, base: &'static str, - routes: &[&Route<'static>]) { + pub fn mount_and_launch(mut self, base: &'static str, routes: &[&Route]) { self.mount(base, routes); self.launch(); } diff --git a/lib/src/request.rs b/lib/src/request.rs index 56fce16b..77cf0118 100644 --- a/lib/src/request.rs +++ b/lib/src/request.rs @@ -1,19 +1,32 @@ use error::Error; use param::FromParam; -pub struct Request; +pub struct Request<'a> { + params: Vec<&'a str>, + uri: &'a str, +} -impl Request { - pub fn empty() -> Request { - Request +impl<'a> Request<'a> { + pub fn empty() -> Request<'static> { + Request::new(vec![], "") } - pub fn get_param_str<'a>(&self, _name: &'a str) -> Result<&'a str, Error> { - Err(Error::NoKey) + pub fn new(params: Vec<&'a str>, uri: &'a str) -> Request<'a> { + Request { + params: params, + uri: uri + } } - pub fn get_param<'b, T: FromParam<'b>>(&self, name: &'b str) - -> Result { - self.get_param_str(name).and_then(T::from_param) + pub fn get_uri(&self) -> &'a str { + self.uri + } + + pub fn get_param>(&'a self, n: usize) -> Result { + if n >= self.params.len() { + Err(Error::NoKey) + } else { + T::from_param(self.params[n]) + } } } diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index 25ac9470..69fa8d77 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -4,7 +4,8 @@ pub trait Collider { fn collides_with(&self, other: &T) -> bool; } -fn check_match_until(break_c: char, a: &str, b: &str, dir: bool) -> bool { +pub fn index_match_until(break_c: char, a: &str, b: &str, dir: bool) + -> Option<(isize, isize)> { let (a_len, b_len) = (a.len() as isize, b.len() as isize); let (mut i, mut j, delta) = if dir { (0, 0, 1) @@ -17,14 +18,18 @@ fn check_match_until(break_c: char, a: &str, b: &str, dir: bool) -> bool { if c1 == break_c || c2 == break_c { break; } else if c1 != c2 { - return false; + return None; } else { i += delta; j += delta; } } - return true; + return Some((i, j)); +} + +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 { @@ -42,7 +47,7 @@ macro_rules! comp_to_str { 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)); - check_match_until('<', a, b, true) && check_match_until('>', a, b, false) + do_match_until('<', a, b, true) && do_match_until('>', a, b, false) } } @@ -52,25 +57,30 @@ mod tests { use router::route::Route; use Method; use Method::*; + use {Request, Response}; type SimpleRoute = (Method, &'static str); + fn dummy_handler(_req: Request) -> Response<'static> { + Response::empty() + } + fn m_collide(a: SimpleRoute, b: SimpleRoute) -> bool { - let route_a = Route::new(a.0, "/", a.1); - route_a.collides_with(&Route::new(b.0, "/", b.1)) + let route_a = Route::new(a.0, "/", a.1, dummy_handler); + route_a.collides_with(&Route::new(b.0, "/", b.1, dummy_handler)) } fn collide(a: &'static str, b: &'static str) -> bool { - let route_a = Route::new(Get, "/", a); - route_a.collides_with(&Route::new(Get, "/", b)) + let route_a = Route::new(Get, "/", a, dummy_handler); + route_a.collides_with(&Route::new(Get, "/", b, dummy_handler)) } fn s_r_collide(a: &'static str, b: &'static str) -> bool { - a.collides_with(&Route::new(Get, "/", b)) + a.collides_with(&Route::new(Get, "/", b, dummy_handler)) } fn r_s_collide(a: &'static str, b: &'static str) -> bool { - let route_a = Route::new(Get, "/", a); + let route_a = Route::new(Get, "/", a, dummy_handler); route_a.collides_with(b) } diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 69b2f3de..0514593a 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -9,6 +9,7 @@ use self::route::Route; use std::collections::hash_map::HashMap; use std::path::Path; use method::Method; +use Handler; type Selector = (Method, usize); @@ -25,25 +26,28 @@ impl Router { // FIXME: Take in Handler. pub fn add_route(&mut self, method: Method, base: &'static str, - route: &'static str) { - let route = Route::new(method, base, route); + route: &'static str, handler: Handler<'static>) { + let route = Route::new(method, base, route, handler); let selector = (method, route.component_count()); self.routes.entry(selector).or_insert(vec![]).push(route); } // TODO: Make a `Router` trait with this function. Rename this `Router` // struct to something like `RocketRouter`. - // TODO: Return an array of matches to the parameters. - pub fn route<'a>(&self, method: Method, uri: &'a str) { + pub fn route<'b>(&'b self, method: Method, uri: &str) -> Option<&'b Route> { + let mut matched_route = None; let path = Path::new(uri); let num_components = path.components().count(); if let Some(routes) = self.routes.get(&(method, num_components)) { - for route in routes { - if route.collides_with(uri) { - println!("Matched {} to: {}", uri, route); + for route in routes.iter().filter(|r| r.collides_with(uri)) { + println!("Matched {} to: {}", uri, route); + if let None = matched_route { + matched_route = Some(route); } } } + + matched_route } pub fn has_collisions(&self) -> bool { @@ -65,3 +69,133 @@ impl Router { } } +#[cfg(test)] +mod test { + use super::Router; + use Method::*; + use {Response, Request}; + + fn dummy_handler(_req: Request) -> Response<'static> { + Response::empty() + } + + fn router_with_routes(routes: &[&'static str]) -> Router { + let mut router = Router::new(); + for route in routes { + router.add_route(Get, "/", route, dummy_handler); + } + + router + } + + #[test] + fn test_collisions() { + let router = router_with_routes(&["/hello", "/hello"]); + assert!(router.has_collisions()); + + let router = router_with_routes(&["/", "/hello"]); + assert!(router.has_collisions()); + + let router = router_with_routes(&["/", "/"]); + assert!(router.has_collisions()); + + let router = router_with_routes(&["/hello/bob", "/hello/"]); + assert!(router.has_collisions()); + } + + #[test] + fn test_ok_routing() { + let router = router_with_routes(&["/hello"]); + assert!(router.route(Get, "/hello").is_some()); + + let router = router_with_routes(&["/"]); + assert!(router.route(Get, "/hello").is_some()); + assert!(router.route(Get, "/hi").is_some()); + assert!(router.route(Get, "/bobbbbbbbbbby").is_some()); + assert!(router.route(Get, "/dsfhjasdf").is_some()); + + let router = router_with_routes(&["//"]); + assert!(router.route(Get, "/hello/hi").is_some()); + assert!(router.route(Get, "/a/b/").is_some()); + assert!(router.route(Get, "/i/a").is_some()); + assert!(router.route(Get, "/jdlk/asdij").is_some()); + + let mut router = Router::new(); + router.add_route(Put, "/", "/hello", dummy_handler); + router.add_route(Post, "/", "/hello", dummy_handler); + router.add_route(Delete, "/", "/hello", dummy_handler); + assert!(router.route(Put, "/hello").is_some()); + assert!(router.route(Post, "/hello").is_some()); + assert!(router.route(Delete, "/hello").is_some()); + } + + #[test] + fn test_err_routing() { + let router = router_with_routes(&["/hello"]); + assert!(router.route(Put, "/hello").is_none()); + assert!(router.route(Post, "/hello").is_none()); + assert!(router.route(Options, "/hello").is_none()); + assert!(router.route(Get, "/hell").is_none()); + assert!(router.route(Get, "/hi").is_none()); + assert!(router.route(Get, "/hello/there").is_none()); + assert!(router.route(Get, "/hello/i").is_none()); + assert!(router.route(Get, "/hillo").is_none()); + + let router = router_with_routes(&["/"]); + assert!(router.route(Put, "/hello").is_none()); + assert!(router.route(Post, "/hello").is_none()); + assert!(router.route(Options, "/hello").is_none()); + assert!(router.route(Get, "/hello/there").is_none()); + assert!(router.route(Get, "/hello/i").is_none()); + + let router = router_with_routes(&["//"]); + assert!(router.route(Get, "/a/b/c").is_none()); + assert!(router.route(Get, "/a").is_none()); + assert!(router.route(Get, "/a/").is_none()); + assert!(router.route(Get, "/a/b/c/d").is_none()); + assert!(router.route(Put, "/hello/hi").is_none()); + assert!(router.route(Put, "/a/b").is_none()); + assert!(router.route(Put, "/a/b").is_none()); + } + + fn match_params(router: &Router, path: &str, expected: &[&str]) -> bool { + router.route(Get, path).map_or(false, |route| { + let params = route.get_params(path); + if params.len() != expected.len() { + return false; + } + + for i in 0..params.len() { + if params[i] != expected[i] { + return false; + } + } + + true + }) + } + + #[test] + fn test_params() { + let router = router_with_routes(&["/"]); + assert!(match_params(&router, "/hello", &["hello"])); + assert!(match_params(&router, "/hi", &["hi"])); + assert!(match_params(&router, "/bob", &["bob"])); + assert!(match_params(&router, "/i", &["i"])); + + let router = router_with_routes(&["/hello"]); + assert!(match_params(&router, "/hello", &[])); + + let router = router_with_routes(&["//"]); + assert!(match_params(&router, "/a/b", &["a", "b"])); + assert!(match_params(&router, "/912/sas", &["912", "sas"])); + + let router = router_with_routes(&["/hello/"]); + assert!(match_params(&router, "/hello/b", &["b"])); + assert!(match_params(&router, "/hello/sergio", &["sergio"])); + + let router = router_with_routes(&["/hello//age"]); + assert!(match_params(&router, "/hello/sergio/age", &["sergio"])); + assert!(match_params(&router, "/hello/you/age", &["you"])); + } +} diff --git a/lib/src/router/route.rs b/lib/src/router/route.rs index 830b7475..aa8b62a9 100644 --- a/lib/src/router/route.rs +++ b/lib/src/router/route.rs @@ -4,35 +4,68 @@ use std::path::{Path, PathBuf}; use std::fmt; use method::Method; use super::Collider; // :D +use std::path::Component; +use Handler; // FIXME: Take in the handler! Or maybe keep that in `Router`? -#[derive(Debug)] pub struct Route { method: Method, - mount: &'static str, - route: &'static str, n_components: usize, + pub handler: Handler<'static>, path: PathBuf } +macro_rules! comp_to_str { + ($component:expr) => ( + match $component { + &Component::Normal(ref comp) => { + if let Some(string) = comp.to_str() { string } + else { panic!("Whoops, no string!") } + }, + &Component::RootDir => "/", + &c@_ => panic!("Whoops, not normal: {:?}!", c) + }; + ) +} + impl Route { - pub fn new(m: Method, mount: &'static str, route: &'static str) -> Route { + pub fn new(m: Method, mount: &'static str, route: &'static str, + handler: Handler<'static>) -> Route { let deduped_path = Route::dedup(mount, route); let path = PathBuf::from(deduped_path); Route { method: m, - mount: mount, - route: route, n_components: path.components().count(), - path: path + handler: handler, + path: path, } } + #[inline] pub fn component_count(&self) -> usize { self.n_components } + // FIXME: This is dirty (the comp_to_str and the RootDir thing). Might need + // to have my own wrapper arround path strings. + // FIXME: Decide whether a component has to be fully variable or not. That + // is, whether you can have: /ab/ + // TODO: Don't return a Vec...take in an &mut [&'a str] (no alloc!) + pub fn get_params<'a>(&self, uri: &'a str) -> Vec<&'a str> { + let mut result = Vec::with_capacity(self.component_count()); + let route_components = self.path.components(); + let uri_components = Path::new(uri).components(); + + for (route_comp, uri_comp) in route_components.zip(uri_components) { + if comp_to_str!(&route_comp).starts_with("<") { + result.push(comp_to_str!(&uri_comp)); + } + } + + result + } + fn dedup(base: &'static str, route: &'static str) -> String { let mut deduped = String::with_capacity(base.len() + route.len() + 1); @@ -56,7 +89,7 @@ impl fmt::Display for Route { } impl Collider for Path { - // FIXME: It's expensive to compute the number of components: O(n) per 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 diff --git a/macros/src/route_decorator.rs b/macros/src/route_decorator.rs index c29e4ffa..8a4d2ce9 100644 --- a/macros/src/route_decorator.rs +++ b/macros/src/route_decorator.rs @@ -189,10 +189,10 @@ pub fn route_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, debug!("Function Declaration: {:?}", fn_decl); let mut fn_param_exprs = vec![]; - for param in &fn_params { + for (i, param) in fn_params.iter().enumerate() { let param_ident = str_to_ident(param.as_str()); let param_fn_item = quote_stmt!(ecx, - let $param_ident = match _req.get_param($param) { + let $param_ident = match _req.get_param($i) { Ok(v) => v, Err(_) => return rocket::Response::not_found() }; @@ -230,7 +230,7 @@ pub fn route_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, let method = method_variant_to_expr(ecx, route_params.method); push(Annotatable::Item(quote_item!(ecx, #[allow(non_upper_case_globals)] - pub static $struct_name: rocket::Route<'static> = rocket::Route { + pub static $struct_name: rocket::Route = rocket::Route { method: $method, path: $path, handler: $route_fn_name