Add 'Request::route' method to get active 'Route'.

This commit also adds the `base` field to `Route` which allows the
base mount point to be retrieved. Finally, this commits adds an
implementation of `FromRequest` for `Route` which returns the active
route if one is available or forwards otherwise.

This commit is a breaking change: it makes `Request` and `MockRequest`
invariant over the lifetime `'r`. While this shouldn't affect most
applications, it may affect some.

Resolves #108.
This commit is contained in:
Sergio Benitez 2017-05-26 19:48:50 -07:00
parent a4292ba666
commit 6a9421935e
6 changed files with 124 additions and 34 deletions

View File

@ -7,10 +7,10 @@ use rocket_contrib::Template;
const TEMPLATE_ROOT: &'static str = "templates/"; const TEMPLATE_ROOT: &'static str = "templates/";
macro_rules! run_test { macro_rules! dispatch {
($req:expr, $test_fn:expr) => ({ ($method:expr, $path:expr, $test_fn:expr) => ({
let rocket = rocket(); let rocket = rocket();
let mut req = $req; let mut req = MockRequest::new($method, $path);
$test_fn(req.dispatch_with(&rocket)); $test_fn(req.dispatch_with(&rocket));
}) })
} }
@ -19,8 +19,7 @@ macro_rules! run_test {
fn test_root() { fn test_root() {
// Check that the redirect works. // Check that the redirect works.
for method in &[Get, Head] { for method in &[Get, Head] {
let req = MockRequest::new(*method, "/"); dispatch!(*method, "/", |mut response: Response| {
run_test!(req, |mut response: Response| {
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert!(response.body().is_none()); assert!(response.body().is_none());
@ -31,8 +30,7 @@ fn test_root() {
// Check that other request methods are not accepted (and instead caught). // Check that other request methods are not accepted (and instead caught).
for method in &[Post, Put, Delete, Options, Trace, Connect, Patch] { for method in &[Post, Put, Delete, Options, Trace, Connect, Patch] {
let req = MockRequest::new(*method, "/"); dispatch!(*method, "/", |mut response: Response| {
run_test!(req, |mut response: Response| {
let mut map = ::std::collections::HashMap::new(); let mut map = ::std::collections::HashMap::new();
map.insert("path", "/"); map.insert("path", "/");
let expected = Template::show(TEMPLATE_ROOT, "error/404", &map).unwrap(); let expected = Template::show(TEMPLATE_ROOT, "error/404", &map).unwrap();
@ -46,16 +44,14 @@ fn test_root() {
#[test] #[test]
fn test_name() { fn test_name() {
// Check that the /hello/<name> route works. // Check that the /hello/<name> route works.
let req = MockRequest::new(Get, "/hello/Jack"); dispatch!(Get, "/hello/Jack", |mut response: Response| {
run_test!(req, |mut response: Response| {
assert_eq!(response.status(), Status::Ok);
let context = super::TemplateContext { let context = super::TemplateContext {
name: "Jack".to_string(), name: "Jack".to_string(),
items: vec!["One", "Two", "Three"].iter().map(|s| s.to_string()).collect() items: vec!["One", "Two", "Three"].iter().map(|s| s.to_string()).collect()
}; };
let expected = Template::show(TEMPLATE_ROOT, "index", &context).unwrap(); let expected = Template::show(TEMPLATE_ROOT, "index", &context).unwrap();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.body_string(), Some(expected)); assert_eq!(response.body_string(), Some(expected));
}); });
} }
@ -63,14 +59,12 @@ fn test_name() {
#[test] #[test]
fn test_404() { fn test_404() {
// Check that the error catcher works. // Check that the error catcher works.
let req = MockRequest::new(Get, "/hello/"); dispatch!(Get, "/hello/", |mut response: Response| {
run_test!(req, |mut response: Response| {
assert_eq!(response.status(), Status::NotFound);
let mut map = ::std::collections::HashMap::new(); let mut map = ::std::collections::HashMap::new();
map.insert("path", "/hello/"); map.insert("path", "/hello/");
let expected = Template::show(TEMPLATE_ROOT, "error/404", &map).unwrap(); let expected = Template::show(TEMPLATE_ROOT, "error/404", &map).unwrap();
assert_eq!(response.status(), Status::NotFound);
assert_eq!(response.body_string(), Some(expected)); assert_eq!(response.body_string(), Some(expected));
}); });
} }

View File

@ -1,6 +1,7 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::net::SocketAddr; use std::net::SocketAddr;
use router::Route;
use request::Request; use request::Request;
use outcome::{self, IntoOutcome}; use outcome::{self, IntoOutcome};
use outcome::Outcome::*; use outcome::Outcome::*;
@ -74,6 +75,13 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// Rocket implements `FromRequest` for several built-in types. Their behavior /// Rocket implements `FromRequest` for several built-in types. Their behavior
/// is documented here. /// is documented here.
/// ///
/// * **Method**
///
/// Extracts the [Method](/rocket/http/enum.Method.html) from the incoming
/// request.
///
/// _This implementation always returns successfully._
///
/// * **&URI** /// * **&URI**
/// ///
/// Extracts the [URI](/rocket/http/uri/struct.URI.html) from the incoming /// Extracts the [URI](/rocket/http/uri/struct.URI.html) from the incoming
@ -81,12 +89,14 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// ///
/// _This implementation always returns successfully._ /// _This implementation always returns successfully._
/// ///
/// * **Method** /// * **&Route**
/// ///
/// Extracts the [Method](/rocket/http/enum.Method.html) from the incoming /// Extracts the [Route](/rocket/struct.Route.html) from the request if one
/// request. /// is available. If a route is not available, the request is forwarded.
/// ///
/// _This implementation always returns successfully._ /// For information of when a route is avaiable, see the
/// [`Request::route`](/rocket/struct.Request.html#method.route)
/// documentation.
/// ///
/// * **Cookies** /// * **Cookies**
/// ///
@ -191,6 +201,14 @@ pub trait FromRequest<'a, 'r>: Sized {
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error>; fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error>;
} }
impl<'a, 'r> FromRequest<'a, 'r> for Method {
type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
Success(request.method())
}
}
impl<'a, 'r> FromRequest<'a, 'r> for &'a URI<'a> { impl<'a, 'r> FromRequest<'a, 'r> for &'a URI<'a> {
type Error = (); type Error = ();
@ -199,11 +217,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for &'a URI<'a> {
} }
} }
impl<'a, 'r> FromRequest<'a, 'r> for Method { impl<'a, 'r> FromRequest<'a, 'r> for &'r Route {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
Success(request.method()) match request.route() {
Some(route) => Success(route),
None => Forward(())
}
} }
} }

View File

@ -1,4 +1,4 @@
use std::cell::RefCell; use std::cell::{Cell, RefCell};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::fmt; use std::fmt;
use std::str; use std::str;
@ -28,6 +28,7 @@ struct PresetState<'r> {
struct RequestState<'r> { struct RequestState<'r> {
preset: Option<PresetState<'r>>, preset: Option<PresetState<'r>>,
params: RefCell<Vec<(usize, usize)>>, params: RefCell<Vec<(usize, usize)>>,
route: Cell<Option<&'r Route>>,
cookies: RefCell<CookieJar>, cookies: RefCell<CookieJar>,
session: RefCell<CookieJar>, session: RefCell<CookieJar>,
accept: Storage<Option<Accept>>, accept: Storage<Option<Accept>>,
@ -64,7 +65,7 @@ impl<'r> Request<'r> {
/// let request = Request::new(Method::Get, "/uri"); /// let request = Request::new(Method::Get, "/uri");
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn new<U: Into<URI<'r>>>(method: Method, uri: U) -> Request<'r> { pub fn new<'s: 'r, U: Into<URI<'s>>>(method: Method, uri: U) -> Request<'r> {
Request { Request {
method: method, method: method,
uri: uri.into(), uri: uri.into(),
@ -72,6 +73,7 @@ impl<'r> Request<'r> {
remote: None, remote: None,
state: RequestState { state: RequestState {
preset: None, preset: None,
route: Cell::new(None),
params: RefCell::new(Vec::new()), params: RefCell::new(Vec::new()),
cookies: RefCell::new(CookieJar::new()), cookies: RefCell::new(CookieJar::new()),
session: RefCell::new(CookieJar::new()), session: RefCell::new(CookieJar::new()),
@ -358,6 +360,18 @@ impl<'r> Request<'r> {
} }
} }
/// Get the limits.
pub fn limits(&self) -> &'r Limits {
&self.preset().config.limits
}
/// Get the current route, if any.
///
/// No route will be avaiable before routing. So not during request fairing.
pub fn route(&self) -> Option<&'r Route> {
self.state.route.get()
}
/// Retrieves and parses into `T` the 0-indexed `n`th dynamic parameter from /// Retrieves and parses into `T` the 0-indexed `n`th dynamic parameter from
/// the request. Returns `Error::NoKey` if `n` is greater than the number of /// the request. Returns `Error::NoKey` if `n` is greater than the number of
/// params. Returns `Error::BadParse` if the parameter type `T` can't be /// params. Returns `Error::BadParse` if the parameter type `T` can't be
@ -450,11 +464,6 @@ impl<'r> Request<'r> {
Some(Segments(&path[i..j])) Some(Segments(&path[i..j]))
} }
/// Get the limits.
pub fn limits(&self) -> &'r Limits {
&self.preset().config.limits
}
#[inline(always)] #[inline(always)]
fn preset(&self) -> &PresetState<'r> { fn preset(&self) -> &PresetState<'r> {
match self.state.preset { match self.state.preset {
@ -471,7 +480,8 @@ impl<'r> Request<'r> {
/// use may result in out of bounds indexing. /// use may result in out of bounds indexing.
/// TODO: Figure out the mount path from here. /// TODO: Figure out the mount path from here.
#[inline] #[inline]
pub(crate) fn set_params(&self, route: &Route) { pub(crate) fn set_route(&self, route: &'r Route) {
self.state.route.set(Some(route));
*self.state.params.borrow_mut() = route.get_param_indexes(self.uri()); *self.state.params.borrow_mut() = route.get_param_indexes(self.uri());
} }

View File

@ -276,15 +276,29 @@ impl Rocket {
/// until one of the handlers returns success or failure, or there are no /// until one of the handlers returns success or failure, or there are no
/// additional routes to try (forward). The corresponding outcome for each /// additional routes to try (forward). The corresponding outcome for each
/// condition is returned. /// condition is returned.
//
// FIXME: We _should_ be able to take an `&mut` here and mutate the request
// at any pointer _before_ we pass it to a handler as long as we drop the
// outcome. That should be safe. Since no mutable borrow can be held
// (ensuring `handler` takes an immutable borrow), any caller to `route`
// should be able to supply an `&mut` and retain an `&` after the call.
//
// Why are we even thinking about this? Because we want to set the route
// that the current request is trying to respond to so guards/handlers can
// get information about it. But we can't use a RefCell<&'r _> since that
// cuases lifetime issues since RefCell contains an UnsafeCell
// (weirdness...). IDEA: Have an `AtomicRef` type that does a pointer swap
// atomically when the ptr's size can be used for atomics.
#[inline] #[inline]
pub(crate) fn route<'r>(&self, request: &'r Request, mut data: Data) pub(crate) fn route<'s, 'r>(&'s self,
-> handler::Outcome<'r> { request: &'r Request<'s>,
mut data: Data) -> handler::Outcome<'r> {
// Go through the list of matching routes until we fail or succeed. // Go through the list of matching routes until we fail or succeed.
let matches = self.router.route(request); let matches = self.router.route(request);
for route in matches { for route in matches {
// Retrieve and set the requests parameters. // Retrieve and set the requests parameters.
info_!("Matched: {}", route); info_!("Matched: {}", route);
request.set_params(route); request.set_route(route);
// Dispatch the request to the handler. // Dispatch the request to the handler.
let outcome = (route.handler)(request, data); let outcome = (route.handler)(request, data);
@ -486,6 +500,7 @@ impl Rocket {
for mut route in routes { for mut route in routes {
let path = format!("{}/{}", base, route.path); let path = format!("{}/{}", base, route.path);
route.set_base(base);
route.set_path(path); route.set_path(path);
info_!("{}", route); info_!("{}", route);

View File

@ -15,7 +15,10 @@ pub struct Route {
pub method: Method, pub method: Method,
/// A function that should be called when the route matches. /// A function that should be called when the route matches.
pub handler: Handler, pub handler: Handler,
/// The path (in Rocket format) that should be matched against. /// The base mount point of this `Route`.
pub base: URI<'static>,
/// The path (in Rocket format) that should be matched against. This path
/// already includes the base mount point.
pub path: URI<'static>, pub path: URI<'static>,
/// The rank of this route. Lower ranks have higher priorities. /// The rank of this route. Lower ranks have higher priorities.
pub rank: isize, pub rank: isize,
@ -48,6 +51,7 @@ impl Route {
method: m, method: m,
handler: handler, handler: handler,
rank: default_rank(&uri), rank: default_rank(&uri),
base: URI::from("/"),
path: uri, path: uri,
format: None, format: None,
} }
@ -59,13 +63,20 @@ impl Route {
{ {
Route { Route {
method: m, method: m,
path: URI::from(path.as_ref().to_string()),
handler: handler, handler: handler,
base: URI::from("/"),
path: URI::from(path.as_ref().to_string()),
rank: rank, rank: rank,
format: None, format: None,
} }
} }
/// Sets the base mount point of the route. Does not update the rank or any
/// other parameters.
pub fn set_base<S>(&mut self, path: S) where S: AsRef<str> {
self.base = URI::from(path.as_ref().to_string());
}
/// Sets the path of the route. Does not update the rank or any other /// Sets the path of the route. Does not update the rank or any other
/// parameters. /// parameters.
pub fn set_path<S>(&mut self, path: S) where S: AsRef<str> { pub fn set_path<S>(&mut self, path: S) where S: AsRef<str> {
@ -104,6 +115,7 @@ impl Clone for Route {
method: self.method, method: self.method,
handler: self.handler, handler: self.handler,
rank: self.rank, rank: self.rank,
base: self.base.clone(),
path: self.path.clone(), path: self.path.clone(),
format: self.format.clone(), format: self.format.clone(),
} }

38
lib/tests/route_guard.rs Normal file
View File

@ -0,0 +1,38 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]
extern crate rocket;
use std::path::PathBuf;
use rocket::Route;
#[get("/<path..>")]
fn files(route: &Route, path: PathBuf) -> String {
format!("{}/{}", route.base.path(), path.to_string_lossy())
}
mod route_guard_tests {
use super::*;
use rocket::Rocket;
use rocket::testing::MockRequest;
use rocket::http::Method::*;
fn assert_path(rocket: &Rocket, path: &str) {
let mut req = MockRequest::new(Get, path);
let mut res = req.dispatch_with(&rocket);
assert_eq!(res.body_string(), Some(path.into()));
}
#[test]
fn check_mount_path() {
let rocket = rocket::ignite()
.mount("/first", routes![files])
.mount("/second", routes![files]);
assert_path(&rocket, "/first/some/path");
assert_path(&rocket, "/second/some/path");
assert_path(&rocket, "/first/second/b/c");
assert_path(&rocket, "/second/a/b/c");
}
}