Implement dynamic request handling via 'Handler' trait.

This commit is contained in:
Sergio Benitez 2017-09-28 16:22:03 -05:00
parent 9bf585496c
commit 29c9cffdbe
8 changed files with 271 additions and 64 deletions

View File

@ -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<MediaType>,
pub handler: Handler,
pub handler: StaticHandler,
pub rank: Option<isize>,
}

View File

@ -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<Response<'r>, 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<Route> {
/// 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<Kind>) -> &'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<Handler>`.
///
/// 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<Handler>;
}
impl<T: Handler + Clone> Cloneable for T {
#[inline(always)]
fn clone_handler(&self) -> Box<Handler> {
Box::new(self.clone())
}
}
impl Clone for Box<Handler> {
#[inline(always)]
fn clone(&self) -> Box<Handler> {
self.clone_handler()
}
}
impl<F: Clone + Sync + Send + 'static> 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>;

View File

@ -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<Client, LaunchError> {
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

View File

@ -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<LaunchError> {
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<Rocket, LaunchError> {
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();

View File

@ -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))

View File

@ -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<Selector, Vec<Route>>, // 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) {
crate fn collisions(mut self) -> Result<Router, Vec<(Route, Route)>> {
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) {
result.push((a_route, 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
if collisions.is_empty() {
Ok(self)
} else {
Err(collisions)
}
// This is slow. Don't expose this publicly; only for tests.
#[cfg(test)]
fn has_collisions(&self) -> bool {
!self.collisions().is_empty()
}
#[inline]
pub fn routes<'a>(&'a self) -> impl Iterator<Item=&'a Route> + '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();

View File

@ -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<Handler>,
/// 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<S>(method: Method, path: S, handler: Handler) -> Route
where S: AsRef<str>
pub fn new<S, H>(method: Method, path: S, handler: H) -> Route
where S: AsRef<str>, 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<S>(rank: isize, method: Method, path: S, handler: Handler) -> Route
where S: AsRef<str>
pub fn ranked<S, H>(rank: isize, method: Method, path: S, handler: H) -> Route
where S: AsRef<str>, 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
}
}

View File

@ -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<Route> {
vec![Route::new(Get, "/<id>", 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])
}