From 29c9cffdbe484cc5ade61906c58e519426331619 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 28 Sep 2017 16:22:03 -0500 Subject: [PATCH] Implement dynamic request handling via 'Handler' trait. --- core/lib/src/codegen.rs | 7 +- core/lib/src/handler.rs | 177 ++++++++++++++++++++++++++++- core/lib/src/local/client.rs | 6 +- core/lib/src/rocket.rs | 30 ++--- core/lib/src/router/collider.rs | 8 +- core/lib/src/router/mod.rs | 64 +++++++---- core/lib/src/router/route.rs | 13 ++- examples/manual_routes/src/main.rs | 30 ++++- 8 files changed, 271 insertions(+), 64 deletions(-) diff --git a/core/lib/src/codegen.rs b/core/lib/src/codegen.rs index 71aab265..cf84330a 100644 --- a/core/lib/src/codegen.rs +++ b/core/lib/src/codegen.rs @@ -1,12 +1,15 @@ -use handler::{Handler, ErrorHandler}; +use {Request, Data}; +use handler::{Outcome, ErrorHandler}; use http::{Method, MediaType}; +pub type StaticHandler = for<'r> fn(&'r Request, Data) -> Outcome<'r>; + pub struct StaticRouteInfo { pub name: &'static str, pub method: Method, pub path: &'static str, pub format: Option, - pub handler: Handler, + pub handler: StaticHandler, pub rank: Option, } diff --git a/core/lib/src/handler.rs b/core/lib/src/handler.rs index 9e27add2..ae7f8715 100644 --- a/core/lib/src/handler.rs +++ b/core/lib/src/handler.rs @@ -1,4 +1,4 @@ -//! The types of request and error handlers and their return values. +//! Types and traits for request and error handlers and their return values. use data::Data; use request::Request; @@ -10,8 +10,179 @@ use outcome; /// Type alias for the `Outcome` of a `Handler`. pub type Outcome<'r> = outcome::Outcome, Status, Data>; -/// The type of a request handler. -pub type Handler = for<'r> fn(&'r Request, Data) -> Outcome<'r>; +/// Trait implemented by types that can handle requests. +/// +/// In general, you will never need to implement `Handler` manually or be +/// concerned about the `Handler` trait; Rocket's code generation handles +/// everything for you. You only need to learn about this trait if you want to +/// provide an external, library-based mechanism to handle requests where +/// request handling depends on input from the user. In other words, if you want +/// to write a plugin for Rocket that looks mostly like a static route but need +/// user provided state to make a request handling decision, you should consider +/// implementing a custom `Handler`. +/// +/// # Example +/// +/// Say you'd like to write a handler that changes its functionality based on an +/// enum value that the user provides: +/// +/// ```rust +/// #[derive(Copy, Clone)] +/// enum Kind { +/// Simple, +/// Intermediate, +/// Complex, +/// } +/// ``` +/// +/// Such a handler might be written and used as follows: +/// +/// ```rust +/// # #[derive(Copy, Clone)] +/// # enum Kind { +/// # Simple, +/// # Intermediate, +/// # Complex, +/// # } +/// use rocket::{Request, Data, Route, http::Method}; +/// use rocket::handler::{self, Handler, Outcome}; +/// +/// #[derive(Clone)] +/// struct CustomHandler(Kind); +/// +/// impl CustomHandler { +/// pub fn new(kind: Kind) -> Vec { +/// vec![Route::new(Method::Get, "/", CustomHandler(kind))] +/// } +/// } +/// +/// impl Handler for CustomHandler { +/// fn handle<'r>(&self, req: &'r Request, data: Data) -> Outcome<'r> { +/// match self.0 { +/// Kind::Simple => Outcome::from(req, "simple"), +/// Kind::Intermediate => Outcome::from(req, "intermediate"), +/// Kind::Complex => Outcome::from(req, "complex"), +/// } +/// } +/// } +/// +/// fn main() { +/// # if false { +/// rocket::ignite() +/// .mount("/", CustomHandler::new(Kind::Simple)) +/// .launch(); +/// # } +/// } +/// ``` +/// +/// Note the following: +/// +/// 1. `CustomHandler` implements `Clone`. This is required so that +/// `CustomHandler` implements `Cloneable` automatically. The `Cloneable` +/// trait serves no other purpose but to ensure that every `Handler` can be +/// cloned, allowing `Route`s to be cloned. +/// 2. The `CustomHandler::new()` method returns a vector of routes so that +/// the user can trivially mount the handler. +/// 3. Unlike static-function-based handlers, this custom handler can make use +/// of any internal state. +/// +/// # Alternatives +/// +/// The previous example could have been implemented using a combination of +/// managed state and a static route, as follows: +/// +/// ```rust +/// # #![feature(plugin, decl_macro)] +/// # #![plugin(rocket_codegen)] +/// # extern crate rocket; +/// # +/// # #[derive(Copy, Clone)] +/// # enum Kind { +/// # Simple, +/// # Intermediate, +/// # Complex, +/// # } +/// # +/// use rocket::State; +/// +/// #[get("/")] +/// fn custom_handler(state: State) -> &'static str { +/// match *state { +/// Kind::Simple => "simple", +/// Kind::Intermediate => "intermediate", +/// Kind::Complex => "complex", +/// } +/// } +/// +/// fn main() { +/// # if false { +/// rocket::ignite() +/// .mount("/", routes![custom_handler]) +/// .manage(Kind::Simple) +/// .launch(); +/// # } +/// } +/// ``` +/// +/// Pros: +/// +/// * The handler is easier to implement since Rocket's code generation +/// ensures type-safety at all levels. +/// +/// Cons: +/// +/// * Only one `Kind` can be stored in managed state. As such, only one +/// variant of the custom handler can be used. +/// * The user must remember to manually call `rocket.manage(state)`. +/// +/// Use this alternative when a single configuration is desired and your custom +/// handler is private to your application. For all other cases, a custom +/// `Handler` implementation is preferred. +pub trait Handler: Cloneable + Send + Sync + 'static { + /// Called by Rocket when a `Request` with its associated `Data` should be + /// handled by this handler. + /// + /// The variant of `Outcome` returned determines what Rocket does next. If + /// the return value is a `Success(Response)`, the wrapped `Response` is + /// used to respond to the client. If the return value is a + /// `Failure(Status)`, the error catcher for `Status` is invoked to generate + /// a response. Otherwise, if the return value is `Forward(Data)`, the next + /// matching route is attempted. If there are no other matching routes, the + /// `404` error catcher is invoked. + fn handle<'r>(&self, request: &'r Request, data: Data) -> Outcome<'r>; +} + +/// Unfortunate but necessary hack to be able to clone a `Box`. +/// +/// This trait should _never_ (and cannot, due to coherence) be implemented by +/// any type. Instead, implement `Clone`. All types that implement `Clone` and +/// `Handler` automatically implement `Cloneable`. +pub trait Cloneable { + fn clone_handler(&self) -> Box; +} + +impl Cloneable for T { + #[inline(always)] + fn clone_handler(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + #[inline(always)] + fn clone(&self) -> Box { + self.clone_handler() + } +} + +impl Handler for F + where for<'r> F: Fn(&'r Request, Data) -> Outcome<'r> +{ + #[inline(always)] + fn handle<'r>(&self, req: &'r Request, data: Data) -> Outcome<'r> { + self(req, data) + } +} /// The type of an error handler. pub type ErrorHandler = for<'r> fn(Error, &'r Request) -> response::Result<'r>; diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index 1076d944..a9dafa6c 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -63,16 +63,12 @@ impl Client { /// is created for cookie tracking. Otherwise, the internal `CookieJar` is /// set to `None`. fn _new(rocket: Rocket, tracked: bool) -> Result { - if let Some(err) = rocket.prelaunch_check() { - return Err(err); - } - let cookies = match tracked { true => Some(RefCell::new(CookieJar::new())), false => None }; - Ok(Client { rocket, cookies }) + Ok(Client { rocket: rocket.prelaunch_check()?, cookies }) } /// Construct a new `Client` from an instance of `Rocket` with cookie diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 7c4212ec..4f02739d 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -286,7 +286,7 @@ impl Rocket { request.set_route(route); // Dispatch the request to the handler. - let outcome = (route.handler)(request, data); + let outcome = route.handler.handle(request, data); // Check if the request processing completed or if the request needs // to be forwarded. If it does, continue the loop to try again. @@ -482,7 +482,7 @@ impl Rocket { /// use rocket::handler::Outcome; /// use rocket::http::Method::*; /// - /// fn hi(req: &Request, _: Data) -> Outcome<'static> { + /// fn hi<'r>(req: &'r Request, _: Data) -> Outcome<'r> { /// Outcome::from(req, "Hello!") /// } /// @@ -666,16 +666,17 @@ impl Rocket { self } - crate fn prelaunch_check(&self) -> Option { - let collisions = self.router.collisions(); - if !collisions.is_empty() { - let owned = collisions.iter().map(|&(a, b)| (a.clone(), b.clone())); - Some(LaunchError::new(LaunchErrorKind::Collision(owned.collect()))) - } else if let Some(failures) = self.fairings.failures() { - Some(LaunchError::new(LaunchErrorKind::FailedFairings(failures.to_vec()))) - } else { - None + crate fn prelaunch_check(mut self) -> Result { + self.router = match self.router.collisions() { + Ok(router) => router, + Err(e) => return Err(LaunchError::new(LaunchErrorKind::Collision(e))) + }; + + if let Some(failures) = self.fairings.failures() { + return Err(LaunchError::new(LaunchErrorKind::FailedFairings(failures.to_vec()))) } + + Ok(self) } /// Starts the application server and begins listening for and dispatching @@ -699,9 +700,10 @@ impl Rocket { /// # } /// ``` pub fn launch(mut self) -> LaunchError { - if let Some(error) = self.prelaunch_check() { - return error; - } + self = match self.prelaunch_check() { + Ok(rocket) => rocket, + Err(launch_error) => return launch_error + }; self.fairings.pretty_print_counts(); diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 1430df34..f9099bac 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -119,19 +119,13 @@ mod tests { use rocket::Rocket; use config::Config; use request::Request; - use data::Data; - use handler::Outcome; - use router::route::Route; + use router::{dummy_handler, route::Route}; use http::{Method, MediaType, ContentType, Accept}; use http::uri::Origin; use http::Method::*; type SimpleRoute = (Method, &'static str); - fn dummy_handler(req: &Request, _: Data) -> Outcome<'static> { - Outcome::from(req, "hi") - } - fn m_collide(a: SimpleRoute, b: SimpleRoute) -> bool { let route_a = Route::new(a.0, a.1.to_string(), dummy_handler); route_a.collides_with(&Route::new(b.0, b.1.to_string(), dummy_handler)) diff --git a/core/lib/src/router/mod.rs b/core/lib/src/router/mod.rs index a5db855f..1fea6ddc 100644 --- a/core/lib/src/router/mod.rs +++ b/core/lib/src/router/mod.rs @@ -3,8 +3,8 @@ mod route; use std::collections::hash_map::HashMap; -use self::collider::Collider; pub use self::route::Route; +use self::collider::Collider; use request::Request; use http::Method; @@ -12,6 +12,11 @@ use http::Method; // type Selector = (Method, usize); type Selector = Method; +// A handler to use when one is needed temporarily. +crate fn dummy_handler<'r>(r: &'r ::Request, _: ::Data) -> ::handler::Outcome<'r> { + ::Outcome::from(r, ()) +} + #[derive(Default)] pub struct Router { routes: HashMap>, // using 'selector' for now @@ -42,36 +47,57 @@ impl Router { matches } - pub fn collisions(&self) -> Vec<(&Route, &Route)> { - let mut result = vec![]; - for routes in self.routes.values() { - for (i, a_route) in routes.iter().enumerate() { - for b_route in routes.iter().skip(i + 1) { - if a_route.collides_with(b_route) { - result.push((a_route, b_route)); + crate fn collisions(mut self) -> Result> { + let mut collisions = vec![]; + for routes in self.routes.values_mut() { + for i in 0..routes.len() { + let (left, right) = routes.split_at_mut(i); + for a_route in left.iter_mut() { + for b_route in right.iter_mut() { + if a_route.collides_with(b_route) { + let dummy_a = Route::new(Method::Get, "/", dummy_handler); + let a = ::std::mem::replace(a_route, dummy_a); + let dummy_b = Route::new(Method::Get, "/", dummy_handler); + let b = ::std::mem::replace(b_route, dummy_b); + collisions.push((a, b)); + } } } } } - result - } - - // This is slow. Don't expose this publicly; only for tests. - #[cfg(test)] - fn has_collisions(&self) -> bool { - !self.collisions().is_empty() + if collisions.is_empty() { + Ok(self) + } else { + Err(collisions) + } } #[inline] pub fn routes<'a>(&'a self) -> impl Iterator + 'a { self.routes.values().flat_map(|v| v.iter()) } + + // This is slow. Don't expose this publicly; only for tests. + #[cfg(test)] + fn has_collisions(&self) -> bool { + for routes in self.routes.values() { + for (i, a_route) in routes.iter().enumerate() { + for b_route in routes.iter().skip(i + 1) { + if a_route.collides_with(b_route) { + return true; + } + } + } + } + + false + } } #[cfg(test)] mod test { - use super::{Router, Route}; + use super::{Router, Route, dummy_handler}; use rocket::Rocket; use config::Config; @@ -79,12 +105,6 @@ mod test { use http::Method::*; use http::uri::Origin; use request::Request; - use data::Data; - use handler::Outcome; - - fn dummy_handler(req: &Request, _: Data) -> Outcome<'static> { - Outcome::from(req, "hi") - } fn router_with_routes(routes: &[&'static str]) -> Router { let mut router = Router::new(); diff --git a/core/lib/src/router/route.rs b/core/lib/src/router/route.rs index f5eb356b..dde9cd09 100644 --- a/core/lib/src/router/route.rs +++ b/core/lib/src/router/route.rs @@ -17,7 +17,7 @@ pub struct Route { /// The method this route matches against. pub method: Method, /// The function that should be called when the route matches. - pub handler: Handler, + pub handler: Box, /// The base mount point of this `Route`. pub base: Origin<'static>, /// The uri (in Rocket's route format) that should be matched against. This @@ -83,8 +83,8 @@ impl Route { /// # Panics /// /// Panics if `path` is not a valid origin URI. - pub fn new(method: Method, path: S, handler: Handler) -> Route - where S: AsRef + pub fn new(method: Method, path: S, handler: H) -> Route + where S: AsRef, H: Handler + 'static { let path = path.as_ref(); let origin = Origin::parse_route(path) @@ -115,8 +115,8 @@ impl Route { /// # Panics /// /// Panics if `path` is not a valid origin URI. - pub fn ranked(rank: isize, method: Method, path: S, handler: Handler) -> Route - where S: AsRef + pub fn ranked(rank: isize, method: Method, path: S, handler: H) -> Route + where S: AsRef, H: Handler + 'static { let uri = Origin::parse_route(path.as_ref()) .expect("invalid URI used as route path in `Route::ranked()`") @@ -126,7 +126,8 @@ impl Route { name: None, format: None, base: Origin::dummy(), - method, handler, rank, uri + handler: Box::new(handler), + method, rank, uri } } diff --git a/examples/manual_routes/src/main.rs b/examples/manual_routes/src/main.rs index e21cf489..e4c76d1b 100644 --- a/examples/manual_routes/src/main.rs +++ b/examples/manual_routes/src/main.rs @@ -6,18 +6,19 @@ mod tests; use std::io; use std::fs::File; -use rocket::{Request, Route, Data, Catcher, Error}; +use rocket::{Request, Handler, Route, Data, Catcher, Error}; use rocket::http::{Status, RawStr}; use rocket::response::{self, Responder}; use rocket::response::status::Custom; use rocket::handler::Outcome; +use rocket::outcome::IntoOutcome; use rocket::http::Method::*; -fn forward(_req: &Request, data: Data) -> Outcome<'static> { +fn forward<'r>(_req: &'r Request, data: Data) -> Outcome<'r> { Outcome::forward(data) } -fn hi(req: &Request, _: Data) -> Outcome<'static> { +fn hi<'r>(req: &'r Request, _: Data) -> Outcome<'r> { Outcome::from(req, "Hello!") } @@ -26,7 +27,7 @@ fn name<'a>(req: &'a Request, _: Data) -> Outcome<'a> { Outcome::from(req, param.map(|r| r.as_str()).unwrap_or("unnamed")) } -fn echo_url(req: &Request, _: Data) -> Outcome<'static> { +fn echo_url<'r>(req: &'r Request, _: Data) -> Outcome<'r> { let param = req.uri() .path() .split_at(6) @@ -55,7 +56,7 @@ fn upload<'r>(req: &'r Request, data: Data) -> Outcome<'r> { } } -fn get_upload(req: &Request, _: Data) -> Outcome<'static> { +fn get_upload<'r>(req: &'r Request, _: Data) -> Outcome<'r> { Outcome::from(req, File::open("/tmp/upload.txt").ok()) } @@ -64,6 +65,24 @@ fn not_found_handler<'r>(_: Error, req: &'r Request) -> response::Result<'r> { res.respond_to(req) } +#[derive(Clone)] +struct CustomHandler { + data: &'static str +} + +impl CustomHandler { + fn new(data: &'static str) -> Vec { + vec![Route::new(Get, "/", Self { data })] + } +} + +impl Handler for CustomHandler { + fn handle<'r>(&self, req: &'r Request, data: Data) -> Outcome<'r> { + let id = req.get_param::<&RawStr>(0).ok().or_forward(data)?; + Outcome::from(req, format!("{} - {}", self.data, id)) + } +} + fn rocket() -> rocket::Rocket { let always_forward = Route::ranked(1, Get, "/", forward); let hello = Route::ranked(2, Get, "/", hi); @@ -80,6 +99,7 @@ fn rocket() -> rocket::Rocket { .mount("/upload", vec![get_upload, post_upload]) .mount("/hello", vec![name.clone()]) .mount("/hi", vec![name]) + .mount("/custom", CustomHandler::new("some data here")) .catch(vec![not_found_catcher]) }