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

View File

@ -1,6 +1,7 @@
use std::fmt::Debug;
use std::net::SocketAddr;
use router::Route;
use request::Request;
use outcome::{self, IntoOutcome};
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
/// is documented here.
///
/// * **Method**
///
/// Extracts the [Method](/rocket/http/enum.Method.html) from the incoming
/// request.
///
/// _This implementation always returns successfully._
///
/// * **&URI**
///
/// 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._
///
/// * **Method**
/// * **&Route**
///
/// Extracts the [Method](/rocket/http/enum.Method.html) from the incoming
/// request.
/// Extracts the [Route](/rocket/struct.Route.html) from the request if one
/// 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**
///
@ -191,6 +201,14 @@ pub trait FromRequest<'a, 'r>: Sized {
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> {
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 = ();
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::fmt;
use std::str;
@ -28,6 +28,7 @@ struct PresetState<'r> {
struct RequestState<'r> {
preset: Option<PresetState<'r>>,
params: RefCell<Vec<(usize, usize)>>,
route: Cell<Option<&'r Route>>,
cookies: RefCell<CookieJar>,
session: RefCell<CookieJar>,
accept: Storage<Option<Accept>>,
@ -64,7 +65,7 @@ impl<'r> Request<'r> {
/// let request = Request::new(Method::Get, "/uri");
/// ```
#[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 {
method: method,
uri: uri.into(),
@ -72,6 +73,7 @@ impl<'r> Request<'r> {
remote: None,
state: RequestState {
preset: None,
route: Cell::new(None),
params: RefCell::new(Vec::new()),
cookies: 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
/// 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
@ -450,11 +464,6 @@ impl<'r> Request<'r> {
Some(Segments(&path[i..j]))
}
/// Get the limits.
pub fn limits(&self) -> &'r Limits {
&self.preset().config.limits
}
#[inline(always)]
fn preset(&self) -> &PresetState<'r> {
match self.state.preset {
@ -471,7 +480,8 @@ impl<'r> Request<'r> {
/// use may result in out of bounds indexing.
/// TODO: Figure out the mount path from here.
#[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());
}

View File

@ -276,15 +276,29 @@ impl Rocket {
/// until one of the handlers returns success or failure, or there are no
/// additional routes to try (forward). The corresponding outcome for each
/// 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]
pub(crate) fn route<'r>(&self, request: &'r Request, mut data: Data)
-> handler::Outcome<'r> {
pub(crate) fn route<'s, 'r>(&'s self,
request: &'r Request<'s>,
mut data: Data) -> handler::Outcome<'r> {
// Go through the list of matching routes until we fail or succeed.
let matches = self.router.route(request);
for route in matches {
// Retrieve and set the requests parameters.
info_!("Matched: {}", route);
request.set_params(route);
request.set_route(route);
// Dispatch the request to the handler.
let outcome = (route.handler)(request, data);
@ -486,6 +500,7 @@ impl Rocket {
for mut route in routes {
let path = format!("{}/{}", base, route.path);
route.set_base(base);
route.set_path(path);
info_!("{}", route);

View File

@ -15,7 +15,10 @@ pub struct Route {
pub method: Method,
/// A function that should be called when the route matches.
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>,
/// The rank of this route. Lower ranks have higher priorities.
pub rank: isize,
@ -48,6 +51,7 @@ impl Route {
method: m,
handler: handler,
rank: default_rank(&uri),
base: URI::from("/"),
path: uri,
format: None,
}
@ -59,13 +63,20 @@ impl Route {
{
Route {
method: m,
path: URI::from(path.as_ref().to_string()),
handler: handler,
base: URI::from("/"),
path: URI::from(path.as_ref().to_string()),
rank: rank,
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
/// parameters.
pub fn set_path<S>(&mut self, path: S) where S: AsRef<str> {
@ -104,6 +115,7 @@ impl Clone for Route {
method: self.method,
handler: self.handler,
rank: self.rank,
base: self.base.clone(),
path: self.path.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");
}
}