diff --git a/core/codegen/src/bang/uri_parsing.rs b/core/codegen/src/bang/uri_parsing.rs index d7c772dc..ea3665a8 100644 --- a/core/codegen/src/bang/uri_parsing.rs +++ b/core/codegen/src/bang/uri_parsing.rs @@ -318,7 +318,7 @@ impl Parse for InternalUriParams { let fn_args = fn_args.into_iter().collect(); input.parse::()?; - let uri_params = input.parse::()?; + let uri_mac = input.parse::()?; let span = route_uri_str.subspan(1..route_uri.path().len() + 1); let path_params = Parameter::parse_many::(route_uri.path().as_str(), span) @@ -334,13 +334,7 @@ impl Parse for InternalUriParams { .collect::>() }).unwrap_or_default(); - Ok(InternalUriParams { - route_uri, - path_params, - query_params, - fn_args, - uri_mac: uri_params - }) + Ok(InternalUriParams { route_uri, path_params, query_params, fn_args, uri_mac }) } } diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs index bef5eedf..e413eccb 100644 --- a/core/codegen/tests/typed-uris.rs +++ b/core/codegen/tests/typed-uris.rs @@ -189,47 +189,58 @@ fn check_route_prefix_suffix() { uri!("/") => "/", uri!("/", index) => "/", uri!("/hi", index) => "/hi", + uri!("/foo", index) => "/foo", + uri!("/hi/", index) => "/hi/", + uri!("/foo/", index) => "/foo/", uri!("/", simple3(10)) => "/?id=10", uri!("/hi", simple3(11)) => "/hi?id=11", + uri!("/hi/", simple3(11)) => "/hi/?id=11", uri!("/mount", simple(100)) => "/mount/100", 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(id = 23)) => "/another/23", uri!("/foo") => "/foo", uri!("/foo/") => "/foo/", uri!("/foo///") => "/foo/", uri!("/foo/bar/") => "/foo/bar/", - uri!("/foo/", index) => "/foo/", - uri!("/foo", index) => "/foo", } 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/foo", index) => "http://rocket.rs/foo", uri!("http://rocket.rs/foo/", index) => "http://rocket.rs/foo/", uri!("http://", index) => "http://", - uri!("ftp:", index) => "ftp:/", + uri!("http:///", index) => "http:///", + uri!("http:////", index) => "http:///", + uri!("ftp:/", index) => "ftp:/", } assert_uri_eq! { 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, "#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, "#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://", 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#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", } let dyn_origin = uri!("/a/b/c"); let dyn_origin2 = uri!("/a/b/c?foo-bar"); + let dyn_origin_slash = uri!("/a/b/c/"); assert_uri_eq! { uri!(dyn_origin.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_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_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_slash = uri!("http://rocket.rs/"); assert_uri_eq! { 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!("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"); @@ -258,12 +282,23 @@ fn check_route_prefix_suffix() { 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"); assert_uri_eq! { 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://", 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", } } @@ -619,3 +654,22 @@ fn test_json() { 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", + } +} diff --git a/core/http/src/uri/fmt/formatter.rs b/core/http/src/uri/fmt/formatter.rs index 9fe18c46..c2e81ac8 100644 --- a/core/http/src/uri/fmt/formatter.rs +++ b/core/http/src/uri/fmt/formatter.rs @@ -437,15 +437,23 @@ impl<'a> ValidRoutePrefix for Origin<'a> { let mut prefix = self.into_normalized(); prefix.clear_query(); + // Avoid a double `//` to start. if prefix.path() == "/" { - // Avoid a double `//` to start. 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); 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) } } @@ -458,12 +466,11 @@ impl<'a> ValidRoutePrefix for Absolute<'a> { let mut prefix = self.into_normalized(); prefix.clear_query(); - if prefix.authority().is_some() { - // The prefix is normalized. Appending a `/` is a no-op. - if path == "/" { - prefix.set_query(query); - return prefix; - } + // Distinguish for routes `/` with bases of `/foo/` and `/foo`. The + // latter base, without a trailing slash, should combine as `/foo`. + if path == "/" { + prefix.set_query(query); + return prefix; } // In these cases, appending `path` would be a no-op or worse. @@ -473,11 +480,7 @@ impl<'a> ValidRoutePrefix for Absolute<'a> { return prefix; } - if path == "/" { - prefix.set_query(query); - return prefix; - } - + // Create the combined URI. prefix.set_path(format!("{}{}", prefix.path(), path)); prefix.set_query(query); prefix diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index e8d0687d..9cb4d608 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -2,6 +2,7 @@ use std::fmt; use std::io::Cursor; use crate::http::uri::Path; +use crate::http::ext::IntoOwned; use crate::response::Response; use crate::request::Request; use crate::http::{Status, ContentType, uri}; @@ -207,9 +208,58 @@ impl Catcher { 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 /// `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 /// 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 @@ -240,10 +290,7 @@ impl Catcher { /// let catcher = catcher.map_base(|base| format!("/foo ? {}", base)); /// assert!(catcher.is_err()); /// ``` - pub fn map_base<'a, F>( - mut self, - mapper: F - ) -> std::result::Result> + pub fn map_base<'a, F>(mut self, mapper: F) -> Result> where F: FnOnce(uri::Origin<'a>) -> String { let new_base = uri::Origin::parse_owned(mapper(self.base))?; diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 99bc6642..e3ceb17b 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -12,7 +12,7 @@ use crate::trip_wire::TripWire; use crate::fairing::{Fairing, Fairings}; use crate::phase::{Phase, Build, Building, Ignite, Igniting, Orbit, Orbiting}; use crate::phase::{Stateful, StateRef, State}; -use crate::http::uri::{self, Origin}; +use crate::http::uri::Origin; use crate::http::ext::IntoOwned; use crate::error::{Error, ErrorKind}; use crate::log::PaintExt; @@ -246,7 +246,7 @@ impl Rocket { fn load<'a, B, T, F, M>(mut self, kind: &str, base: B, items: Vec, m: M, f: F) -> Self where B: TryInto> + Clone + fmt::Display, B::Error: fmt::Display, - M: Fn(&Origin<'a>, T) -> Result>, + M: Fn(&Origin<'a>, T) -> T, F: Fn(&mut Self, T), T: Clone + fmt::Display, { @@ -266,42 +266,65 @@ impl Rocket { } for unmounted_item in items { - let item = match 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) + f(&mut self, m(&base, unmounted_item.clone())) } self } - /// Mounts all of the routes in the supplied vector at the given `base` - /// path. Mounting a route with path `path` at path `base` makes the route - /// available at `base/path`. + /// Mounts all of the `routes` at the given `base` mount point. + /// + /// 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 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 - /// were generated using Rocket's code generation._ + /// * any route URI is not a valid origin URI. (**Note:** _This kind of + /// panic is guaranteed not to occur if the routes were generated using + /// Rocket's code generation._) /// /// # Examples /// /// Use the `routes!` macro to mount routes created using the code - /// generation facilities. Requests to the `/hello/world` URI will be - /// dispatched to the `hi` route. + /// generation facilities. Requests to both `/world` and `/hello/world` URI + /// will be dispatched to the `hi` route. /// /// ```rust,no_run /// # #[macro_use] extern crate rocket; @@ -313,7 +336,9 @@ impl Rocket { /// /// #[launch] /// fn rocket() -> _ { - /// rocket::build().mount("/hello", routes![hi]) + /// rocket::build() + /// .mount("/", routes![hi]) + /// .mount("/hello", routes![hi]) /// } /// ``` /// @@ -344,7 +369,7 @@ impl Rocket { R: 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)) } @@ -383,7 +408,7 @@ impl Rocket { C: 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)) } diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 25f8436f..131d2a30 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::convert::From; use std::borrow::Cow; 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` /// with the returned base. /// + /// **Note:** Prefer to use [`Route::rebase()`] whenever possible! + /// /// `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 /// a query, it is ignored. Returns an error if the base produced by @@ -269,18 +307,28 @@ impl Route { /// /// ```rust /// use rocket::Route; - /// use rocket::http::{Method, uri::Origin}; + /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; + /// # use rocket::uri; /// /// let index = Route::new(Method::Get, "/foo/bar", handler); /// assert_eq!(index.uri.base(), "/"); /// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); /// assert_eq!(index.uri.path(), "/foo/bar"); /// - /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); - /// assert_eq!(index.uri.base(), "/boo"); - /// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); - /// assert_eq!(index.uri.path(), "/boo/foo/bar"); + /// # let old_index = index; + /// # let index = old_index.clone(); + /// let mapped = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); + /// 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> where F: FnOnce(uri::Origin<'a>) -> String @@ -298,11 +346,7 @@ impl fmt::Display for Route { } write!(f, "{} ", Paint::green(&self.method))?; - if self.uri.base() != "/" { - write!(f, "{}", Paint::blue(self.uri.base()).underline())?; - } - - write!(f, "{}", Paint::blue(&self.uri.unmounted()))?; + self.uri.color_fmt(f)?; if self.rank > 1 { write!(f, " [{}]", Paint::default(&self.rank).bold())?; diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index 9fc073b0..9ef156ba 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -98,7 +98,7 @@ impl<'a> RouteUri<'a> { /// Panics if `base` or `uri` cannot be parsed as `Origin`s. #[track_caller] 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`. @@ -110,7 +110,7 @@ impl<'a> RouteUri<'a> { pub fn try_new(base: &str, uri: &str) -> Result> { let mut base = Origin::parse(base) .map_err(|e| e.into_owned())? - .into_normalized_nontrailing() + .into_normalized() .into_owned(); base.clear_query(); @@ -120,16 +120,17 @@ impl<'a> RouteUri<'a> { .into_normalized() .into_owned(); - let compiled_uri = match base.path().as_str() { - "/" => origin.to_string(), - base => match (origin.path().as_str(), origin.query()) { - ("/", None) => base.to_string(), - ("/", Some(q)) => format!("{}?{}", base, q), - _ => format!("{}{}", base, origin), + // Distinguish for routes `/` with bases of `/foo/` and `/foo`. The + // latter base, without a trailing slash, should combine as `/foo`. + let route_uri = match origin.path().as_str() { + "/" if !base.has_trailing_slash() => match origin.query() { + Some(query) => format!("{}?{}", base, query), + 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())? .into_normalized() .into_owned(); @@ -171,12 +172,16 @@ impl<'a> RouteUri<'a> { /// use rocket::Route; /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; + /// # use rocket::uri; /// /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); /// 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"); + /// + /// let route = route.rebase(uri!("/foo")); + /// assert_eq!(route.uri.base(), "/foo/boo"); /// ``` #[inline(always)] pub fn base(&self) -> Path<'_> { @@ -191,9 +196,10 @@ impl<'a> RouteUri<'a> { /// use rocket::Route; /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; + /// # use rocket::uri; /// /// 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.base(), "/boo"); @@ -232,6 +238,23 @@ impl<'a> RouteUri<'a> { // We subtract `3` because `raw_path` is never `0`: 0b0100 = 4 - 3 = 1. -((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 { @@ -289,7 +312,7 @@ impl<'a> std::ops::Deref for RouteUri<'a> { impl fmt::Display for RouteUri<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner().fmt(f) + self.uri.fmt(f) } } @@ -304,3 +327,46 @@ impl PartialEq for RouteUri<'_> { impl PartialEq<&str> for RouteUri<'_> { 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/"); + } +}