Introduce more flexible mounting.

Prior to this commit, a route with a URI of `/` could not be mounted in
such a way that the resulting effective URI contained a trailing slash.
This commit changes the semantics of mounting so that mounting such a
route to a mount point with a trailing slash yields an effective URI
with a trailing slash. When mounted to points without a trailing slash,
the effective URI does not have a trailing slash.

This commit also introduces the `Route::rebase()` and
`Catcher::rebase()` methods for easier rebasing of existing routes and
catchers.

Finally, this commit improves logging such that mount points of `/`
are underlined in the logs.

Tests and docs were added and modified as necessary.

Resolves #2533.
This commit is contained in:
Sergio Benitez 2023-05-03 19:56:49 -07:00
parent dbc43c41a3
commit 56cf905c6e
7 changed files with 313 additions and 80 deletions

View File

@ -318,7 +318,7 @@ impl Parse for InternalUriParams {
let fn_args = fn_args.into_iter().collect(); let fn_args = fn_args.into_iter().collect();
input.parse::<Token![,]>()?; input.parse::<Token![,]>()?;
let uri_params = input.parse::<RoutedUri>()?; let uri_mac = input.parse::<RoutedUri>()?;
let span = route_uri_str.subspan(1..route_uri.path().len() + 1); let span = route_uri_str.subspan(1..route_uri.path().len() + 1);
let path_params = Parameter::parse_many::<fmt::Path>(route_uri.path().as_str(), span) let path_params = Parameter::parse_many::<fmt::Path>(route_uri.path().as_str(), span)
@ -334,13 +334,7 @@ impl Parse for InternalUriParams {
.collect::<Vec<_>>() .collect::<Vec<_>>()
}).unwrap_or_default(); }).unwrap_or_default();
Ok(InternalUriParams { Ok(InternalUriParams { route_uri, path_params, query_params, fn_args, uri_mac })
route_uri,
path_params,
query_params,
fn_args,
uri_mac: uri_params
})
} }
} }

View File

@ -189,47 +189,58 @@ fn check_route_prefix_suffix() {
uri!("/") => "/", uri!("/") => "/",
uri!("/", index) => "/", uri!("/", index) => "/",
uri!("/hi", index) => "/hi", uri!("/hi", index) => "/hi",
uri!("/foo", index) => "/foo",
uri!("/hi/", index) => "/hi/",
uri!("/foo/", index) => "/foo/",
uri!("/", simple3(10)) => "/?id=10", uri!("/", simple3(10)) => "/?id=10",
uri!("/hi", simple3(11)) => "/hi?id=11", uri!("/hi", simple3(11)) => "/hi?id=11",
uri!("/hi/", simple3(11)) => "/hi/?id=11",
uri!("/mount", simple(100)) => "/mount/100", uri!("/mount", simple(100)) => "/mount/100",
uri!("/mount", simple(id = 23)) => "/mount/23", uri!("/mount", simple(id = 23)) => "/mount/23",
uri!("/mount/", simple(100)) => "/mount/100",
uri!("/mount/", simple(id = 23)) => "/mount/23",
uri!("/another", simple(100)) => "/another/100", uri!("/another", simple(100)) => "/another/100",
uri!("/another", simple(id = 23)) => "/another/23", uri!("/another", simple(id = 23)) => "/another/23",
uri!("/foo") => "/foo", uri!("/foo") => "/foo",
uri!("/foo/") => "/foo/", uri!("/foo/") => "/foo/",
uri!("/foo///") => "/foo/", uri!("/foo///") => "/foo/",
uri!("/foo/bar/") => "/foo/bar/", uri!("/foo/bar/") => "/foo/bar/",
uri!("/foo/", index) => "/foo/",
uri!("/foo", index) => "/foo",
} }
assert_uri_eq! { assert_uri_eq! {
uri!("http://rocket.rs", index) => "http://rocket.rs", uri!("http://rocket.rs", index) => "http://rocket.rs",
uri!("http://rocket.rs/", index) => "http://rocket.rs/", uri!("http://rocket.rs/", index) => "http://rocket.rs/",
uri!("http://rocket.rs///", index) => "http://rocket.rs/",
uri!("http://rocket.rs/foo", index) => "http://rocket.rs/foo", uri!("http://rocket.rs/foo", index) => "http://rocket.rs/foo",
uri!("http://rocket.rs/foo/", index) => "http://rocket.rs/foo/", uri!("http://rocket.rs/foo/", index) => "http://rocket.rs/foo/",
uri!("http://", index) => "http://", uri!("http://", index) => "http://",
uri!("ftp:", index) => "ftp:/", uri!("http:///", index) => "http:///",
uri!("http:////", index) => "http:///",
uri!("ftp:/", index) => "ftp:/",
} }
assert_uri_eq! { assert_uri_eq! {
uri!("http://rocket.rs", index, "?foo") => "http://rocket.rs?foo", uri!("http://rocket.rs", index, "?foo") => "http://rocket.rs?foo",
uri!("http://rocket.rs", index, "?") => "http://rocket.rs?", uri!("http://rocket.rs", index, "?") => "http://rocket.rs?",
uri!("http://rocket.rs", index, "#") => "http://rocket.rs#", uri!("http://rocket.rs", index, "#") => "http://rocket.rs#",
uri!("http://rocket.rs", index, "#bar") => "http://rocket.rs#bar",
uri!("http://rocket.rs", index, "?bar#baz") => "http://rocket.rs?bar#baz",
uri!("http://rocket.rs/", index, "?foo") => "http://rocket.rs/?foo",
uri!("http://rocket.rs/", index, "?") => "http://rocket.rs/?", uri!("http://rocket.rs/", index, "?") => "http://rocket.rs/?",
uri!("http://rocket.rs/", index, "#") => "http://rocket.rs/#", uri!("http://rocket.rs/", index, "#") => "http://rocket.rs/#",
uri!("http://rocket.rs", index, "#bar") => "http://rocket.rs#bar",
uri!("http://rocket.rs/", index, "#bar") => "http://rocket.rs/#bar", uri!("http://rocket.rs/", index, "#bar") => "http://rocket.rs/#bar",
uri!("http://rocket.rs", index, "?bar#baz") => "http://rocket.rs?bar#baz",
uri!("http://rocket.rs/", index, "?bar#baz") => "http://rocket.rs/?bar#baz", uri!("http://rocket.rs/", index, "?bar#baz") => "http://rocket.rs/?bar#baz",
uri!("http://", index, "?foo") => "http://?foo", uri!("http://", index, "?foo") => "http://?foo",
uri!("http://rocket.rs", simple3(id = 100), "?foo") => "http://rocket.rs?id=100", uri!("http://rocket.rs", simple3(id = 100), "?foo") => "http://rocket.rs?id=100",
uri!("http://rocket.rs", simple3(id = 100), "?foo#bar") => "http://rocket.rs?id=100#bar", uri!("http://rocket.rs", simple3(id = 100), "?foo#bar") => "http://rocket.rs?id=100#bar",
uri!("http://rocket.rs/", simple3(id = 100), "?foo") => "http://rocket.rs/?id=100",
uri!("http://rocket.rs/", simple3(id = 100), "?foo#bar") => "http://rocket.rs/?id=100#bar",
uri!(_, simple3(id = 100), "?foo#bar") => "/?id=100#bar", uri!(_, simple3(id = 100), "?foo#bar") => "/?id=100#bar",
} }
let dyn_origin = uri!("/a/b/c"); let dyn_origin = uri!("/a/b/c");
let dyn_origin2 = uri!("/a/b/c?foo-bar"); let dyn_origin2 = uri!("/a/b/c?foo-bar");
let dyn_origin_slash = uri!("/a/b/c/");
assert_uri_eq! { assert_uri_eq! {
uri!(dyn_origin.clone(), index) => "/a/b/c", uri!(dyn_origin.clone(), index) => "/a/b/c",
uri!(dyn_origin2.clone(), index) => "/a/b/c", uri!(dyn_origin2.clone(), index) => "/a/b/c",
@ -241,12 +252,25 @@ fn check_route_prefix_suffix() {
uri!(dyn_origin2.clone(), simple2(100, "hey")) => "/a/b/c/100/hey", uri!(dyn_origin2.clone(), simple2(100, "hey")) => "/a/b/c/100/hey",
uri!(dyn_origin.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey", uri!(dyn_origin.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey",
uri!(dyn_origin2.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey", uri!(dyn_origin2.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey",
uri!(dyn_origin_slash.clone(), index) => "/a/b/c/",
uri!(dyn_origin_slash.clone(), simple3(10)) => "/a/b/c/?id=10",
uri!(dyn_origin_slash.clone(), simple(100)) => "/a/b/c/100",
} }
let dyn_absolute = uri!("http://rocket.rs"); let dyn_absolute = uri!("http://rocket.rs");
let dyn_absolute_slash = uri!("http://rocket.rs/");
assert_uri_eq! { assert_uri_eq! {
uri!(dyn_absolute.clone(), index) => "http://rocket.rs", uri!(dyn_absolute.clone(), index) => "http://rocket.rs",
uri!(dyn_absolute.clone(), simple(100)) => "http://rocket.rs/100",
uri!(dyn_absolute.clone(), simple3(123)) => "http://rocket.rs?id=123",
uri!(dyn_absolute_slash.clone(), index) => "http://rocket.rs/",
uri!(dyn_absolute_slash.clone(), simple(100)) => "http://rocket.rs/100",
uri!(dyn_absolute_slash.clone(), simple3(123)) => "http://rocket.rs/?id=123",
uri!(uri!("http://rocket.rs/a/b"), index) => "http://rocket.rs/a/b", uri!(uri!("http://rocket.rs/a/b"), index) => "http://rocket.rs/a/b",
uri!("http://rocket.rs/a/b") => "http://rocket.rs/a/b",
uri!(uri!("http://rocket.rs/a/b"), index) => "http://rocket.rs/a/b",
uri!("http://rocket.rs/a/b") => "http://rocket.rs/a/b",
} }
let dyn_abs = uri!("http://rocket.rs?foo"); let dyn_abs = uri!("http://rocket.rs?foo");
@ -258,12 +282,23 @@ fn check_route_prefix_suffix() {
uri!(_, simple3(id = 123), dyn_abs) => "/?id=123", uri!(_, simple3(id = 123), dyn_abs) => "/?id=123",
} }
let dyn_abs = uri!("http://rocket.rs/?foo");
assert_uri_eq! {
uri!(_, index, dyn_abs.clone()) => "/?foo",
uri!("http://rocket.rs", index, dyn_abs.clone()) => "http://rocket.rs?foo",
uri!("http://rocket.rs/", index, dyn_abs.clone()) => "http://rocket.rs/?foo",
uri!("http://", index, dyn_abs.clone()) => "http://?foo",
uri!("http:///", index, dyn_abs.clone()) => "http:///?foo",
uri!(_, simple3(id = 123), dyn_abs) => "/?id=123",
}
let dyn_ref = uri!("?foo#bar"); let dyn_ref = uri!("?foo#bar");
assert_uri_eq! { assert_uri_eq! {
uri!(_, index, dyn_ref.clone()) => "/?foo#bar", uri!(_, index, dyn_ref.clone()) => "/?foo#bar",
uri!("http://rocket.rs", index, dyn_ref.clone()) => "http://rocket.rs?foo#bar", uri!("http://rocket.rs", index, dyn_ref.clone()) => "http://rocket.rs?foo#bar",
uri!("http://rocket.rs/", index, dyn_ref.clone()) => "http://rocket.rs/?foo#bar", uri!("http://rocket.rs/", index, dyn_ref.clone()) => "http://rocket.rs/?foo#bar",
uri!("http://", index, dyn_ref.clone()) => "http://?foo#bar", uri!("http://", index, dyn_ref.clone()) => "http://?foo#bar",
uri!("http:///", index, dyn_ref.clone()) => "http:///?foo#bar",
uri!(_, simple3(id = 123), dyn_ref) => "/?id=123#bar", uri!(_, simple3(id = 123), dyn_ref) => "/?id=123#bar",
} }
} }
@ -619,3 +654,22 @@ fn test_json() {
uri!(bar(&mut Json(inner))) => "/?json=%7B%22foo%22:%7B%22foo%22:%22hi%22%7D%7D", uri!(bar(&mut Json(inner))) => "/?json=%7B%22foo%22:%7B%22foo%22:%22hi%22%7D%7D",
} }
} }
#[test]
fn test_route_uri_normalization_with_prefix() {
#[get("/world")] fn world() {}
assert_uri_eq! {
uri!("/", index()) => "/",
uri!("/foo", index()) => "/foo",
uri!("/bar/", index()) => "/bar/",
uri!("/foo/bar", index()) => "/foo/bar",
uri!("/foo/bar/", index()) => "/foo/bar/",
uri!("/", world()) => "/world",
uri!("/foo", world()) => "/foo/world",
uri!("/bar/", world()) => "/bar/world",
uri!("/foo/bar", world()) => "/foo/bar/world",
uri!("/foo/bar/", world()) => "/foo/bar/world",
}
}

View File

@ -437,15 +437,23 @@ impl<'a> ValidRoutePrefix for Origin<'a> {
let mut prefix = self.into_normalized(); let mut prefix = self.into_normalized();
prefix.clear_query(); prefix.clear_query();
// Avoid a double `//` to start.
if prefix.path() == "/" { if prefix.path() == "/" {
// Avoid a double `//` to start.
return Origin::new(path, query); return Origin::new(path, query);
} else if path == "/" { }
// Appending path to `/` is a no-op, but append any query.
// Avoid allocating if the `path` would result in just the prefix.
if path == "/" {
prefix.set_query(query); prefix.set_query(query);
return prefix; return prefix;
} }
// Avoid a `//` resulting from joining.
if prefix.has_trailing_slash() && path.starts_with('/') {
return Origin::new(format!("{}{}", prefix.path(), &path[1..]), query);
}
// Join normally.
Origin::new(format!("{}{}", prefix.path(), path), query) Origin::new(format!("{}{}", prefix.path(), path), query)
} }
} }
@ -458,12 +466,11 @@ impl<'a> ValidRoutePrefix for Absolute<'a> {
let mut prefix = self.into_normalized(); let mut prefix = self.into_normalized();
prefix.clear_query(); prefix.clear_query();
if prefix.authority().is_some() { // Distinguish for routes `/` with bases of `/foo/` and `/foo`. The
// The prefix is normalized. Appending a `/` is a no-op. // latter base, without a trailing slash, should combine as `/foo`.
if path == "/" { if path == "/" {
prefix.set_query(query); prefix.set_query(query);
return prefix; return prefix;
}
} }
// In these cases, appending `path` would be a no-op or worse. // In these cases, appending `path` would be a no-op or worse.
@ -473,11 +480,7 @@ impl<'a> ValidRoutePrefix for Absolute<'a> {
return prefix; return prefix;
} }
if path == "/" { // Create the combined URI.
prefix.set_query(query);
return prefix;
}
prefix.set_path(format!("{}{}", prefix.path(), path)); prefix.set_path(format!("{}{}", prefix.path(), path));
prefix.set_query(query); prefix.set_query(query);
prefix prefix

View File

@ -2,6 +2,7 @@ use std::fmt;
use std::io::Cursor; use std::io::Cursor;
use crate::http::uri::Path; use crate::http::uri::Path;
use crate::http::ext::IntoOwned;
use crate::response::Response; use crate::response::Response;
use crate::request::Request; use crate::request::Request;
use crate::http::{Status, ContentType, uri}; use crate::http::{Status, ContentType, uri};
@ -207,9 +208,58 @@ impl Catcher {
self.base.path() self.base.path()
} }
/// Prefix `base` to the current `base` in `self.`
///
/// If the the current base is `/`, then the base is replaced by `base`.
/// Otherwise, `base` is prefixed to the existing `base`.
///
/// ```rust
/// use rocket::request::Request;
/// use rocket::catcher::{Catcher, BoxFuture};
/// use rocket::response::Responder;
/// use rocket::http::Status;
/// # use rocket::uri;
///
/// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
/// let res = (status, format!("404: {}", req.uri()));
/// Box::pin(async move { res.respond_to(req) })
/// }
///
/// let catcher = Catcher::new(404, handle_404);
/// assert_eq!(catcher.base(), "/");
///
/// // Since the base is `/`, rebasing replaces the base.
/// let rebased = catcher.rebase(uri!("/boo"));
/// assert_eq!(rebased.base(), "/boo");
///
/// // Now every rebase prefixes.
/// let rebased = rebased.rebase(uri!("/base"));
/// assert_eq!(rebased.base(), "/base/boo");
///
/// // Note that trailing slashes have no effect and are thus removed:
/// let catcher = Catcher::new(404, handle_404);
/// let rebased = catcher.rebase(uri!("/boo/"));
/// assert_eq!(rebased.base(), "/boo");
/// ```
pub fn rebase(mut self, mut base: uri::Origin<'_>) -> Self {
self.base = if self.base.path() == "/" {
base.clear_query();
base.into_normalized_nontrailing().into_owned()
} else {
uri::Origin::parse_owned(format!("{}{}", base.path(), self.base))
.expect("catcher rebase: {new}{old} is valid origin URI")
.into_normalized_nontrailing()
};
self.rank = -1 * (self.base().segments().filter(|s| !s.is_empty()).count() as isize);
self
}
/// Maps the `base` of this catcher using `mapper`, returning a new /// Maps the `base` of this catcher using `mapper`, returning a new
/// `Catcher` with the returned base. /// `Catcher` with the returned base.
/// ///
/// **Note:** Prefer to use [`Catcher::rebase()`] whenever possible!
///
/// `mapper` is called with the current base. The returned `String` is used /// `mapper` is called with the current base. The returned `String` is used
/// as the new base if it is a valid URI. If the returned base URI contains /// as the new base if it is a valid URI. If the returned base URI contains
/// a query, it is ignored. Returns an error if the base produced by /// a query, it is ignored. Returns an error if the base produced by
@ -240,10 +290,7 @@ impl Catcher {
/// let catcher = catcher.map_base(|base| format!("/foo ? {}", base)); /// let catcher = catcher.map_base(|base| format!("/foo ? {}", base));
/// assert!(catcher.is_err()); /// assert!(catcher.is_err());
/// ``` /// ```
pub fn map_base<'a, F>( pub fn map_base<'a, F>(mut self, mapper: F) -> Result<Self, uri::Error<'static>>
mut self,
mapper: F
) -> std::result::Result<Self, uri::Error<'static>>
where F: FnOnce(uri::Origin<'a>) -> String where F: FnOnce(uri::Origin<'a>) -> String
{ {
let new_base = uri::Origin::parse_owned(mapper(self.base))?; let new_base = uri::Origin::parse_owned(mapper(self.base))?;

View File

@ -12,7 +12,7 @@ use crate::trip_wire::TripWire;
use crate::fairing::{Fairing, Fairings}; use crate::fairing::{Fairing, Fairings};
use crate::phase::{Phase, Build, Building, Ignite, Igniting, Orbit, Orbiting}; use crate::phase::{Phase, Build, Building, Ignite, Igniting, Orbit, Orbiting};
use crate::phase::{Stateful, StateRef, State}; use crate::phase::{Stateful, StateRef, State};
use crate::http::uri::{self, Origin}; use crate::http::uri::Origin;
use crate::http::ext::IntoOwned; use crate::http::ext::IntoOwned;
use crate::error::{Error, ErrorKind}; use crate::error::{Error, ErrorKind};
use crate::log::PaintExt; use crate::log::PaintExt;
@ -246,7 +246,7 @@ impl Rocket<Build> {
fn load<'a, B, T, F, M>(mut self, kind: &str, base: B, items: Vec<T>, m: M, f: F) -> Self fn load<'a, B, T, F, M>(mut self, kind: &str, base: B, items: Vec<T>, m: M, f: F) -> Self
where B: TryInto<Origin<'a>> + Clone + fmt::Display, where B: TryInto<Origin<'a>> + Clone + fmt::Display,
B::Error: fmt::Display, B::Error: fmt::Display,
M: Fn(&Origin<'a>, T) -> Result<T, uri::Error<'static>>, M: Fn(&Origin<'a>, T) -> T,
F: Fn(&mut Self, T), F: Fn(&mut Self, T),
T: Clone + fmt::Display, T: Clone + fmt::Display,
{ {
@ -266,42 +266,65 @@ impl Rocket<Build> {
} }
for unmounted_item in items { for unmounted_item in items {
let item = match m(&base, unmounted_item.clone()) { f(&mut self, m(&base, unmounted_item.clone()))
Ok(item) => item,
Err(e) => {
error!("malformed URI in {} {}", kind, unmounted_item);
error_!("{}", e);
info_!("{} {}", Paint::white("in"), std::panic::Location::caller());
panic!("aborting due to invalid {} URI", kind);
}
};
f(&mut self, item)
} }
self self
} }
/// Mounts all of the routes in the supplied vector at the given `base` /// Mounts all of the `routes` at the given `base` mount point.
/// path. Mounting a route with path `path` at path `base` makes the route ///
/// available at `base/path`. /// A route _mounted_ at `base` has an effective URI of `base/route`, where
/// `route` is the route URI. In other words, `base` is added as a prefix to
/// the route's URI. The URI resulting from joining the `base` URI and the
/// route URI is called the route's _effective URI_, as this is the URI used
/// for request matching during routing.
///
/// A `base` URI is not allowed to have a query part. If a `base` _does_
/// have a query part, it is ignored when producing the effective URI.
///
/// A `base` may have an optional trailing slash. A route with a URI path of
/// `/` (and any optional query) mounted at a `base` has an effective URI
/// equal to the `base` (plus any optional query). That is, if the base has
/// a trailing slash, the effective URI path has a trailing slash, and
/// otherwise it does not. Routes with URI paths other than `/` are not
/// effected by trailing slashes in their corresponding mount point.
///
/// As concrete examples, consider the following table:
///
/// | mount point | route URI | effective URI |
/// |-------------|-----------|---------------|
/// | `/` | `/foo` | `/foo` |
/// | `/` | `/foo/` | `/foo/` |
/// | `/foo` | `/` | `/foo` |
/// | `/foo` | `/?bar` | `/foo?bar` |
/// | `/foo` | `/bar` | `/foo/bar` |
/// | `/foo` | `/bar/` | `/foo/bar/` |
/// | `/foo/` | `/` | `/foo/` |
/// | `/foo/` | `/bar` | `/foo/bar` |
/// | `/foo/` | `/?bar` | `/foo/?bar` |
/// | `/foo/bar` | `/` | `/foo/bar` |
/// | `/foo/bar/` | `/` | `/foo/bar/` |
/// | `/foo/?bar` | `/` | `/foo/` |
/// | `/foo/?bar` | `/baz` | `/foo/baz` |
/// | `/foo/?bar` | `/baz/` | `/foo/baz/` |
/// ///
/// # Panics /// # Panics
/// ///
/// Panics if either: /// Panics if either:
/// * the `base` mount point is not a valid static path: a valid origin
/// URI without dynamic parameters.
/// ///
/// * any route's URI is not a valid origin URI. /// * the `base` mount point is not a valid origin URI without dynamic
/// parameters
/// ///
/// **Note:** _This kind of panic is guaranteed not to occur if the routes /// * any route URI is not a valid origin URI. (**Note:** _This kind of
/// were generated using Rocket's code generation._ /// panic is guaranteed not to occur if the routes were generated using
/// Rocket's code generation._)
/// ///
/// # Examples /// # Examples
/// ///
/// Use the `routes!` macro to mount routes created using the code /// Use the `routes!` macro to mount routes created using the code
/// generation facilities. Requests to the `/hello/world` URI will be /// generation facilities. Requests to both `/world` and `/hello/world` URI
/// dispatched to the `hi` route. /// will be dispatched to the `hi` route.
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
@ -313,7 +336,9 @@ impl Rocket<Build> {
/// ///
/// #[launch] /// #[launch]
/// fn rocket() -> _ { /// fn rocket() -> _ {
/// rocket::build().mount("/hello", routes![hi]) /// rocket::build()
/// .mount("/", routes![hi])
/// .mount("/hello", routes![hi])
/// } /// }
/// ``` /// ```
/// ///
@ -344,7 +369,7 @@ impl Rocket<Build> {
R: Into<Vec<Route>> R: Into<Vec<Route>>
{ {
self.load("route", base, routes.into(), self.load("route", base, routes.into(),
|base, route| route.map_base(|old| format!("{}{}", base, old)), |base, route| route.rebase(base.clone()),
|r, route| r.0.routes.push(route)) |r, route| r.0.routes.push(route))
} }
@ -383,7 +408,7 @@ impl Rocket<Build> {
C: Into<Vec<Catcher>> C: Into<Vec<Catcher>>
{ {
self.load("catcher", base, catchers.into(), self.load("catcher", base, catchers.into(),
|base, catcher| catcher.map_base(|old| format!("{}{}", base, old)), |base, catcher| catcher.rebase(base.clone()),
|r, catcher| r.0.catchers.push(catcher)) |r, catcher| r.0.catchers.push(catcher))
} }

View File

@ -1,5 +1,4 @@
use std::fmt; use std::fmt;
use std::convert::From;
use std::borrow::Cow; use std::borrow::Cow;
use yansi::Paint; use yansi::Paint;
@ -257,9 +256,48 @@ impl Route {
} }
} }
/// Prefix `base` to any existing mount point base in `self`.
///
/// If the the current mount point base is `/`, then the base is replaced by
/// `base`. Otherwise, `base` is prefixed to the existing `base`.
///
/// ```rust
/// use rocket::Route;
/// use rocket::http::Method;
/// # use rocket::route::dummy_handler as handler;
/// # use rocket::uri;
///
/// // The default base is `/`.
/// let index = Route::new(Method::Get, "/foo/bar", handler);
///
/// // Since the base is `/`, rebasing replaces the base.
/// let rebased = index.rebase(uri!("/boo"));
/// assert_eq!(rebased.uri.base(), "/boo");
///
/// // Now every rebase prefixes.
/// let rebased = rebased.rebase(uri!("/base"));
/// assert_eq!(rebased.uri.base(), "/base/boo");
///
/// // Note that trailing slashes are preserved:
/// let index = Route::new(Method::Get, "/foo", handler);
/// let rebased = index.rebase(uri!("/boo/"));
/// assert_eq!(rebased.uri.base(), "/boo/");
/// ```
pub fn rebase(mut self, base: uri::Origin<'_>) -> Self {
let new_base = match self.uri.base().as_str() {
"/" => base.path().to_string(),
_ => format!("{}{}", base.path(), self.uri.base()),
};
self.uri = RouteUri::new(&new_base, &self.uri.unmounted_origin.to_string());
self
}
/// Maps the `base` of this route using `mapper`, returning a new `Route` /// Maps the `base` of this route using `mapper`, returning a new `Route`
/// with the returned base. /// with the returned base.
/// ///
/// **Note:** Prefer to use [`Route::rebase()`] whenever possible!
///
/// `mapper` is called with the current base. The returned `String` is used /// `mapper` is called with the current base. The returned `String` is used
/// as the new base if it is a valid URI. If the returned base URI contains /// as the new base if it is a valid URI. If the returned base URI contains
/// a query, it is ignored. Returns an error if the base produced by /// a query, it is ignored. Returns an error if the base produced by
@ -269,18 +307,28 @@ impl Route {
/// ///
/// ```rust /// ```rust
/// use rocket::Route; /// use rocket::Route;
/// use rocket::http::{Method, uri::Origin}; /// use rocket::http::Method;
/// # use rocket::route::dummy_handler as handler; /// # use rocket::route::dummy_handler as handler;
/// # use rocket::uri;
/// ///
/// let index = Route::new(Method::Get, "/foo/bar", handler); /// let index = Route::new(Method::Get, "/foo/bar", handler);
/// assert_eq!(index.uri.base(), "/"); /// assert_eq!(index.uri.base(), "/");
/// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); /// assert_eq!(index.uri.unmounted().path(), "/foo/bar");
/// assert_eq!(index.uri.path(), "/foo/bar"); /// assert_eq!(index.uri.path(), "/foo/bar");
/// ///
/// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); /// # let old_index = index;
/// assert_eq!(index.uri.base(), "/boo"); /// # let index = old_index.clone();
/// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); /// let mapped = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap();
/// assert_eq!(index.uri.path(), "/boo/foo/bar"); /// assert_eq!(mapped.uri.base(), "/boo/");
/// assert_eq!(mapped.uri.unmounted().path(), "/foo/bar");
/// assert_eq!(mapped.uri.path(), "/boo/foo/bar");
///
/// // Note that this produces different `base` results than `rebase`!
/// # let index = old_index.clone();
/// let rebased = index.rebase(uri!("/boo"));
/// assert_eq!(rebased.uri.base(), "/boo");
/// assert_eq!(rebased.uri.unmounted().path(), "/foo/bar");
/// assert_eq!(rebased.uri.path(), "/boo/foo/bar");
/// ``` /// ```
pub fn map_base<'a, F>(mut self, mapper: F) -> Result<Self, uri::Error<'static>> pub fn map_base<'a, F>(mut self, mapper: F) -> Result<Self, uri::Error<'static>>
where F: FnOnce(uri::Origin<'a>) -> String where F: FnOnce(uri::Origin<'a>) -> String
@ -298,11 +346,7 @@ impl fmt::Display for Route {
} }
write!(f, "{} ", Paint::green(&self.method))?; write!(f, "{} ", Paint::green(&self.method))?;
if self.uri.base() != "/" { self.uri.color_fmt(f)?;
write!(f, "{}", Paint::blue(self.uri.base()).underline())?;
}
write!(f, "{}", Paint::blue(&self.uri.unmounted()))?;
if self.rank > 1 { if self.rank > 1 {
write!(f, " [{}]", Paint::default(&self.rank).bold())?; write!(f, " [{}]", Paint::default(&self.rank).bold())?;

View File

@ -98,7 +98,7 @@ impl<'a> RouteUri<'a> {
/// Panics if `base` or `uri` cannot be parsed as `Origin`s. /// Panics if `base` or `uri` cannot be parsed as `Origin`s.
#[track_caller] #[track_caller]
pub(crate) fn new(base: &str, uri: &str) -> RouteUri<'static> { pub(crate) fn new(base: &str, uri: &str) -> RouteUri<'static> {
Self::try_new(base, uri).expect("Expected valid URIs") Self::try_new(base, uri).expect("expected valid route URIs")
} }
/// Creates a new `RouteUri` from a `base` mount point and a route `uri`. /// Creates a new `RouteUri` from a `base` mount point and a route `uri`.
@ -110,7 +110,7 @@ impl<'a> RouteUri<'a> {
pub fn try_new(base: &str, uri: &str) -> Result<RouteUri<'static>> { pub fn try_new(base: &str, uri: &str) -> Result<RouteUri<'static>> {
let mut base = Origin::parse(base) let mut base = Origin::parse(base)
.map_err(|e| e.into_owned())? .map_err(|e| e.into_owned())?
.into_normalized_nontrailing() .into_normalized()
.into_owned(); .into_owned();
base.clear_query(); base.clear_query();
@ -120,16 +120,17 @@ impl<'a> RouteUri<'a> {
.into_normalized() .into_normalized()
.into_owned(); .into_owned();
let compiled_uri = match base.path().as_str() { // Distinguish for routes `/` with bases of `/foo/` and `/foo`. The
"/" => origin.to_string(), // latter base, without a trailing slash, should combine as `/foo`.
base => match (origin.path().as_str(), origin.query()) { let route_uri = match origin.path().as_str() {
("/", None) => base.to_string(), "/" if !base.has_trailing_slash() => match origin.query() {
("/", Some(q)) => format!("{}?{}", base, q), Some(query) => format!("{}?{}", base, query),
_ => format!("{}{}", base, origin), None => base.to_string(),
} }
_ => format!("{}{}", base, origin),
}; };
let uri = Origin::parse_route(&compiled_uri) let uri = Origin::parse_route(&route_uri)
.map_err(|e| e.into_owned())? .map_err(|e| e.into_owned())?
.into_normalized() .into_normalized()
.into_owned(); .into_owned();
@ -171,12 +172,16 @@ impl<'a> RouteUri<'a> {
/// use rocket::Route; /// use rocket::Route;
/// use rocket::http::Method; /// use rocket::http::Method;
/// # use rocket::route::dummy_handler as handler; /// # use rocket::route::dummy_handler as handler;
/// # use rocket::uri;
/// ///
/// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler);
/// assert_eq!(route.uri.base(), "/"); /// assert_eq!(route.uri.base(), "/");
/// ///
/// let route = route.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); /// let route = route.rebase(uri!("/boo"));
/// assert_eq!(route.uri.base(), "/boo"); /// assert_eq!(route.uri.base(), "/boo");
///
/// let route = route.rebase(uri!("/foo"));
/// assert_eq!(route.uri.base(), "/foo/boo");
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn base(&self) -> Path<'_> { pub fn base(&self) -> Path<'_> {
@ -191,9 +196,10 @@ impl<'a> RouteUri<'a> {
/// use rocket::Route; /// use rocket::Route;
/// use rocket::http::Method; /// use rocket::http::Method;
/// # use rocket::route::dummy_handler as handler; /// # use rocket::route::dummy_handler as handler;
/// # use rocket::uri;
/// ///
/// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler);
/// let route = route.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); /// let route = route.rebase(uri!("/boo"));
/// ///
/// assert_eq!(route.uri, "/boo/foo/bar?a=1"); /// assert_eq!(route.uri, "/boo/foo/bar?a=1");
/// assert_eq!(route.uri.base(), "/boo"); /// assert_eq!(route.uri.base(), "/boo");
@ -232,6 +238,23 @@ impl<'a> RouteUri<'a> {
// We subtract `3` because `raw_path` is never `0`: 0b0100 = 4 - 3 = 1. // We subtract `3` because `raw_path` is never `0`: 0b0100 = 4 - 3 = 1.
-((raw_weight as isize) - 3) -((raw_weight as isize) - 3)
} }
pub(crate) fn color_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use yansi::Paint;
let (path, base, unmounted) = (self.uri.path(), self.base(), self.unmounted().path());
let unmounted_part = path.strip_prefix(base.as_str())
.map(|raw| raw.as_str())
.unwrap_or(unmounted.as_str());
write!(f, "{}", Paint::blue(self.base()).underline())?;
write!(f, "{}", Paint::blue(unmounted_part))?;
if let Some(q) = self.unmounted().query() {
write!(f, "?{}", Paint::green(q))?;
}
Ok(())
}
} }
impl Metadata { impl Metadata {
@ -289,7 +312,7 @@ impl<'a> std::ops::Deref for RouteUri<'a> {
impl fmt::Display for RouteUri<'_> { impl fmt::Display for RouteUri<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner().fmt(f) self.uri.fmt(f)
} }
} }
@ -304,3 +327,46 @@ impl PartialEq<str> for RouteUri<'_> {
impl PartialEq<&str> for RouteUri<'_> { impl PartialEq<&str> for RouteUri<'_> {
fn eq(&self, other: &&str) -> bool { self.inner() == *other } fn eq(&self, other: &&str) -> bool { self.inner() == *other }
} }
#[cfg(test)]
mod tests {
macro_rules! assert_uri_equality {
($base:expr, $path:expr => $ebase:expr, $epath:expr, $efull:expr) => {
let uri = super::RouteUri::new($base, $path);
assert_eq!(uri, $efull, "complete URI mismatch. expected {}, got {}", $efull, uri);
assert_eq!(uri.base(), $ebase, "expected base {}, got {}", $ebase, uri.base());
assert_eq!(uri.unmounted(), $epath, "expected unmounted {}, got {}", $epath,
uri.unmounted());
};
}
#[test]
fn test_route_uri_composition() {
assert_uri_equality!("/", "/" => "/", "/", "/");
assert_uri_equality!("/", "/foo" => "/", "/foo", "/foo");
assert_uri_equality!("/", "/foo/bar" => "/", "/foo/bar", "/foo/bar");
assert_uri_equality!("/", "/foo/" => "/", "/foo/", "/foo/");
assert_uri_equality!("/", "/foo/bar/" => "/", "/foo/bar/", "/foo/bar/");
assert_uri_equality!("/foo", "/" => "/foo", "/", "/foo");
assert_uri_equality!("/foo", "/bar" => "/foo", "/bar", "/foo/bar");
assert_uri_equality!("/foo", "/bar/" => "/foo", "/bar/", "/foo/bar/");
assert_uri_equality!("/foo", "/?baz" => "/foo", "/?baz", "/foo?baz");
assert_uri_equality!("/foo", "/bar?baz" => "/foo", "/bar?baz", "/foo/bar?baz");
assert_uri_equality!("/foo", "/bar/?baz" => "/foo", "/bar/?baz", "/foo/bar/?baz");
assert_uri_equality!("/foo/", "/" => "/foo/", "/", "/foo/");
assert_uri_equality!("/foo/", "/bar" => "/foo/", "/bar", "/foo/bar");
assert_uri_equality!("/foo/", "/bar/" => "/foo/", "/bar/", "/foo/bar/");
assert_uri_equality!("/foo/", "/?baz" => "/foo/", "/?baz", "/foo/?baz");
assert_uri_equality!("/foo/", "/bar?baz" => "/foo/", "/bar?baz", "/foo/bar?baz");
assert_uri_equality!("/foo/", "/bar/?baz" => "/foo/", "/bar/?baz", "/foo/bar/?baz");
assert_uri_equality!("/foo?baz", "/" => "/foo", "/", "/foo");
assert_uri_equality!("/foo?baz", "/bar" => "/foo", "/bar", "/foo/bar");
assert_uri_equality!("/foo?baz", "/bar/" => "/foo", "/bar/", "/foo/bar/");
assert_uri_equality!("/foo/?baz", "/" => "/foo/", "/", "/foo/");
assert_uri_equality!("/foo/?baz", "/bar" => "/foo/", "/bar", "/foo/bar");
assert_uri_equality!("/foo/?baz", "/bar/" => "/foo/", "/bar/", "/foo/bar/");
}
}