mirror of https://github.com/rwf2/Rocket.git
Expose 'Route', 'Catcher' collision and matching.
This commit exposes four new methods: * `Route::collides_with(&Route)` * `Route::matches(&Request)` * `Catcher::collides_with(&Catcher)` * `Catcher::matches(Status, &Request)` Each method checks the corresponding condition: whether two routes collide, whether a route matches a request, whether two catchers collide, and whether a catcher matches an error arising from a request. This functionality is used internally by Rocket to make routing decisions. By exposing these methods, external libraries can use guaranteed consistent logic to check the same routing conditions. Resolves #1561.
This commit is contained in:
parent
0c80f7d9e0
commit
b61ac6eb18
|
@ -32,16 +32,21 @@ use yansi::Paint;
|
||||||
///
|
///
|
||||||
/// # Routing
|
/// # Routing
|
||||||
///
|
///
|
||||||
/// An error arising from a particular request _matches_ a catcher _iff_:
|
/// If a route fails by returning a failure [`Outcome`], Rocket routes the
|
||||||
|
/// erroring request to the highest precedence catcher among all the catchers
|
||||||
|
/// that [match](Catcher::matches()). See [`Catcher::matches()`] for details on
|
||||||
|
/// matching. Precedence is determined by the catcher's _base_, which is
|
||||||
|
/// provided as the first argument to [`Rocket::register()`]. Catchers with more
|
||||||
|
/// non-empty segments have a higher precedence.
|
||||||
///
|
///
|
||||||
/// * It is a default catcher _or_ has a status code matching the error code.
|
/// Rocket provides [built-in defaults](#built-in-default), but _default_
|
||||||
/// * Its base is a prefix of the normalized/decoded request URI path.
|
/// catchers can also be registered. A _default_ catcher is a catcher with no
|
||||||
|
/// explicit status code: `None`.
|
||||||
///
|
///
|
||||||
/// A _default_ catcher is a catcher with no explicit status code: `None`. The
|
/// [`Outcome`]: crate::request::Outcome
|
||||||
/// catcher's _base_ is provided as the first argument to
|
/// [`Rocket::register()`]: crate::Rocket::register()
|
||||||
/// [`Rocket::register()`](crate::Rocket::register()).
|
|
||||||
///
|
///
|
||||||
/// # Collisions
|
/// ## Collisions
|
||||||
///
|
///
|
||||||
/// Two catchers are said to _collide_ if there exists an error that matches
|
/// Two catchers are said to _collide_ if there exists an error that matches
|
||||||
/// both catchers. Colliding catchers present a routing ambiguity and are thus
|
/// both catchers. Colliding catchers present a routing ambiguity and are thus
|
||||||
|
@ -50,7 +55,7 @@ use yansi::Paint;
|
||||||
/// after it becomes statically impossible to register any more catchers on an
|
/// after it becomes statically impossible to register any more catchers on an
|
||||||
/// instance of `Rocket`.
|
/// instance of `Rocket`.
|
||||||
///
|
///
|
||||||
/// ### Built-In Default
|
/// ## Built-In Default
|
||||||
///
|
///
|
||||||
/// Rocket's provides a built-in default catcher that can handle all errors. It
|
/// Rocket's provides a built-in default catcher that can handle all errors. It
|
||||||
/// produces HTML or JSON, depending on the value of the `Accept` header. As
|
/// produces HTML or JSON, depending on the value of the `Accept` header. As
|
||||||
|
@ -119,14 +124,8 @@ pub struct Catcher {
|
||||||
|
|
||||||
/// The catcher's calculated rank.
|
/// The catcher's calculated rank.
|
||||||
///
|
///
|
||||||
/// This is [base.segments().len() | base.chars().len()].
|
/// This is -(number of nonempty segments in base).
|
||||||
pub(crate) rank: u64,
|
pub(crate) rank: isize,
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_rank(base: &uri::Origin<'_>) -> u64 {
|
|
||||||
let major = u32::MAX - base.path().segments().num() as u32;
|
|
||||||
let minor = u32::MAX - base.path().as_str().chars().count() as u32;
|
|
||||||
((major as u64) << 32) | (minor as u64)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Catcher {
|
impl Catcher {
|
||||||
|
@ -178,7 +177,7 @@ impl Catcher {
|
||||||
name: None,
|
name: None,
|
||||||
base: uri::Origin::ROOT,
|
base: uri::Origin::ROOT,
|
||||||
handler: Box::new(handler),
|
handler: Box::new(handler),
|
||||||
rank: compute_rank(&uri::Origin::ROOT),
|
rank: 0,
|
||||||
code
|
code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,7 +249,7 @@ impl Catcher {
|
||||||
let new_base = uri::Origin::parse_owned(mapper(self.base))?;
|
let new_base = uri::Origin::parse_owned(mapper(self.base))?;
|
||||||
self.base = new_base.into_normalized_nontrailing();
|
self.base = new_base.into_normalized_nontrailing();
|
||||||
self.base.clear_query();
|
self.base.clear_query();
|
||||||
self.rank = compute_rank(&self.base);
|
self.rank = -1 * (self.base().segments().filter(|s| !s.is_empty()).count() as isize);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,8 @@ impl<F: Clone + Sync + Send + 'static> Handler for F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// Used in tests! Do not use, please.
|
||||||
|
#[doc(hidden)]
|
||||||
pub fn dummy_handler<'r>(_: Status, _: &'r Request<'_>) -> BoxFuture<'r> {
|
pub fn dummy_handler<'r>(_: Status, _: &'r Request<'_>) -> BoxFuture<'r> {
|
||||||
Box::pin(async move { Ok(Response::new()) })
|
Box::pin(async move { Ok(Response::new()) })
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,33 +38,22 @@ use crate::sentinel::Sentry;
|
||||||
///
|
///
|
||||||
/// # Routing
|
/// # Routing
|
||||||
///
|
///
|
||||||
/// A request _matches_ a route _iff_:
|
/// A request is _routed_ to a route if it has the highest precedence (lowest
|
||||||
|
/// rank) among all routes that [match](Route::matches()) the request. See
|
||||||
|
/// [`Route::matches()`] for details on what it means for a request to match.
|
||||||
///
|
///
|
||||||
/// * The route's method matches that of the incoming request.
|
/// Note that a single request _may_ be routed to multiple routes if a route
|
||||||
/// * The route's format (if any) matches that of the incoming request.
|
/// forwards. If a route fails, the request is instead routed to the highest
|
||||||
/// - If route specifies a format, it only matches requests for that format.
|
/// precedence [`Catcher`](crate::Catcher).
|
||||||
/// - If route doesn't specify a format, it matches requests for any format.
|
|
||||||
/// - A route's `format` matches against the `Accept` header in the request
|
|
||||||
/// when the route's method [`supports_payload()`] and `Content-Type`
|
|
||||||
/// header otherwise.
|
|
||||||
/// - Non-specific `Accept` header components (`*`) match anything.
|
|
||||||
/// * All static components in the route's path match the corresponding
|
|
||||||
/// components in the same position in the incoming request.
|
|
||||||
/// * All static components in the route's query string are also in the
|
|
||||||
/// request query string, though in any position. If there is no query
|
|
||||||
/// in the route, requests with and without queries match.
|
|
||||||
///
|
///
|
||||||
/// Rocket routes requests to matching routes.
|
/// ## Collisions
|
||||||
///
|
///
|
||||||
/// [`supports_payload()`]: Method::supports_payload()
|
/// Two routes are said to [collide](Route::collides_with()) if there exists a
|
||||||
///
|
/// request that matches both routes. Colliding routes present a routing
|
||||||
/// # Collisions
|
/// ambiguity and are thus disallowed by Rocket. Because routes can be
|
||||||
///
|
/// constructed dynamically, collision checking is done at
|
||||||
/// Two routes are said to _collide_ if there exists a request that matches both
|
/// [`ignite`](crate::Rocket::ignite()) time, after it becomes statically
|
||||||
/// routes. Colliding routes present a routing ambiguity and are thus disallowed
|
/// impossible to add any more routes to an instance of `Rocket`.
|
||||||
/// by Rocket. Because routes can be constructed dynamically, collision checking
|
|
||||||
/// is done at [`ignite`](crate::Rocket::ignite()) time, after it becomes
|
|
||||||
/// statically impossible to add any more routes to an instance of `Rocket`.
|
|
||||||
///
|
///
|
||||||
/// Note that because query parsing is always lenient -- extra and missing query
|
/// Note that because query parsing is always lenient -- extra and missing query
|
||||||
/// parameters are allowed -- queries do not directly impact whether two routes
|
/// parameters are allowed -- queries do not directly impact whether two routes
|
||||||
|
@ -300,12 +289,6 @@ impl Route {
|
||||||
self.uri = RouteUri::try_new(&base, &self.uri.unmounted_origin.to_string())?;
|
self.uri = RouteUri::try_new(&base, &self.uri.unmounted_origin.to_string())?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if `self` collides with `other`.
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn collides_with(&self, other: &Route) -> bool {
|
|
||||||
crate::router::Collide::collides_with(self, other)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Route {
|
impl fmt::Display for Route {
|
||||||
|
|
|
@ -7,22 +7,86 @@ pub trait Collide<T = Self> {
|
||||||
fn collides_with(&self, other: &T) -> bool;
|
fn collides_with(&self, other: &T) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collide for Route {
|
impl Route {
|
||||||
/// Determines if two routes can match against some request. That is, if two
|
/// Returns `true` if `self` collides with `other`.
|
||||||
/// routes `collide`, there exists a request that can match against both
|
|
||||||
/// routes.
|
|
||||||
///
|
///
|
||||||
/// This implementation is used at initialization to check if two user
|
/// A [_collision_](Route#collisions) between two routes occurs when there
|
||||||
/// routes collide before launching. Format collisions works like this:
|
/// exists a request that could [match](Route::matches()) either route. That
|
||||||
|
/// is, a routing ambiguity would ensue if both routes were made available
|
||||||
|
/// to the router.
|
||||||
///
|
///
|
||||||
/// * If route specifies a format, it only gets requests for that format.
|
/// Specifically, a collision occurs when two routes `a` and `b`:
|
||||||
/// * If route doesn't specify a format, it gets requests for any format.
|
|
||||||
///
|
///
|
||||||
/// Because query parsing is lenient, and dynamic query parameters can be
|
/// * Have the same [method](Route::method).
|
||||||
/// missing, the particularities of a query string do not impact whether two
|
/// * Have the same [rank](Route#default-ranking).
|
||||||
/// routes collide. The query effects the route's color, however, which
|
/// * The routes' methods don't support a payload _or_ the routes'
|
||||||
/// effects its rank.
|
/// methods support a payload and the formats overlap. Formats overlap
|
||||||
fn collides_with(&self, other: &Route) -> bool {
|
/// when:
|
||||||
|
/// - The top-level type of either is `*` or the top-level types are
|
||||||
|
/// equivalent.
|
||||||
|
/// - The sub-level type of either is `*` or the sub-level types are
|
||||||
|
/// equivalent.
|
||||||
|
/// * Have overlapping route URIs. This means that either:
|
||||||
|
/// - The URIs have the same number of segments `n`, and for `i` in
|
||||||
|
/// `0..n`, either `a.uri[i]` is dynamic _or_ `b.uri[i]` is dynamic
|
||||||
|
/// _or_ they're both static with the same value.
|
||||||
|
/// - One URI has fewer segments _and_ ends with a trailing dynamic
|
||||||
|
/// parameter _and_ the preceeding segments in both routes match the
|
||||||
|
/// conditions above.
|
||||||
|
///
|
||||||
|
/// Collisions are symmetric: for any routes `a` and `b`,
|
||||||
|
/// `a.collides_with(b) => b.collides_with(a)`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rocket::Route;
|
||||||
|
/// use rocket::http::{Method, MediaType};
|
||||||
|
/// # use rocket::route::dummy_handler as handler;
|
||||||
|
///
|
||||||
|
/// // Two routes with the same method, rank, URI, and formats collide.
|
||||||
|
/// let a = Route::new(Method::Get, "/", handler);
|
||||||
|
/// let b = Route::new(Method::Get, "/", handler);
|
||||||
|
/// assert!(a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two routes with the same method, rank, URI, and overlapping formats.
|
||||||
|
/// let mut a = Route::new(Method::Post, "/", handler);
|
||||||
|
/// a.format = Some(MediaType::new("*", "custom"));
|
||||||
|
/// let mut b = Route::new(Method::Post, "/", handler);
|
||||||
|
/// b.format = Some(MediaType::new("text", "*"));
|
||||||
|
/// assert!(a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two routes with different ranks don't collide.
|
||||||
|
/// let a = Route::ranked(1, Method::Get, "/", handler);
|
||||||
|
/// let b = Route::ranked(2, Method::Get, "/", handler);
|
||||||
|
/// assert!(!a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two routes with different methods don't collide.
|
||||||
|
/// let a = Route::new(Method::Put, "/", handler);
|
||||||
|
/// let b = Route::new(Method::Post, "/", handler);
|
||||||
|
/// assert!(!a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two routes with non-overlapping URIs do not collide.
|
||||||
|
/// let a = Route::new(Method::Get, "/foo", handler);
|
||||||
|
/// let b = Route::new(Method::Get, "/bar/<baz>", handler);
|
||||||
|
/// assert!(!a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two payload-supporting routes with non-overlapping formats.
|
||||||
|
/// let mut a = Route::new(Method::Post, "/", handler);
|
||||||
|
/// a.format = Some(MediaType::HTML);
|
||||||
|
/// let mut b = Route::new(Method::Post, "/", handler);
|
||||||
|
/// b.format = Some(MediaType::JSON);
|
||||||
|
/// assert!(!a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two non payload-supporting routes with non-overlapping formats
|
||||||
|
/// // collide. A request with `Accept: */*` matches both.
|
||||||
|
/// let mut a = Route::new(Method::Get, "/", handler);
|
||||||
|
/// a.format = Some(MediaType::HTML);
|
||||||
|
/// let mut b = Route::new(Method::Get, "/", handler);
|
||||||
|
/// b.format = Some(MediaType::JSON);
|
||||||
|
/// assert!(a.collides_with(&b));
|
||||||
|
/// ```
|
||||||
|
pub fn collides_with(&self, other: &Route) -> bool {
|
||||||
self.method == other.method
|
self.method == other.method
|
||||||
&& self.rank == other.rank
|
&& self.rank == other.rank
|
||||||
&& self.uri.collides_with(&other.uri)
|
&& self.uri.collides_with(&other.uri)
|
||||||
|
@ -30,16 +94,68 @@ impl Collide for Route {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collide for Catcher {
|
impl Catcher {
|
||||||
/// Determines if two catchers are in conflict: there exists a request for
|
/// Returns `true` if `self` collides with `other`.
|
||||||
/// which there exist no rule to determine _which_ of the two catchers to
|
|
||||||
/// use. This means that the catchers:
|
|
||||||
///
|
///
|
||||||
/// * Have the same base.
|
/// A [_collision_](Catcher#collisions) between two catchers occurs when
|
||||||
/// * Have the same status code or are both defaults.
|
/// there exists a request and ensuing error that could
|
||||||
|
/// [match](Catcher::matches()) both catchers. That is, a routing ambiguity
|
||||||
|
/// would ensue if both catchers were made available to the router.
|
||||||
|
///
|
||||||
|
/// Specifically, a collision occurs when two catchers:
|
||||||
|
///
|
||||||
|
/// * Have the same [base](Catcher::base()).
|
||||||
|
/// * Have the same status [code](Catcher::code) or are both `default`.
|
||||||
|
///
|
||||||
|
/// Collisions are symmetric: for any catchers `a` and `b`,
|
||||||
|
/// `a.collides_with(b) => b.collides_with(a)`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rocket::Catcher;
|
||||||
|
/// # use rocket::catcher::dummy_handler as handler;
|
||||||
|
///
|
||||||
|
/// // Two catchers with the same status code and base collide.
|
||||||
|
/// let a = Catcher::new(404, handler).map_base(|_| format!("/foo")).unwrap();
|
||||||
|
/// let b = Catcher::new(404, handler).map_base(|_| format!("/foo")).unwrap();
|
||||||
|
/// assert!(a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two catchers with a different base _do not_ collide.
|
||||||
|
/// let a = Catcher::new(404, handler);
|
||||||
|
/// let b = a.clone().map_base(|_| format!("/bar")).unwrap();
|
||||||
|
/// assert_eq!(a.base(), "/");
|
||||||
|
/// assert_eq!(b.base(), "/bar");
|
||||||
|
/// assert!(!a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // Two catchers with a different codes _do not_ collide.
|
||||||
|
/// let a = Catcher::new(404, handler);
|
||||||
|
/// let b = Catcher::new(500, handler);
|
||||||
|
/// assert_eq!(a.base(), "/");
|
||||||
|
/// assert_eq!(b.base(), "/");
|
||||||
|
/// assert!(!a.collides_with(&b));
|
||||||
|
///
|
||||||
|
/// // A catcher _with_ a status code and one _without_ do not collide.
|
||||||
|
/// let a = Catcher::new(404, handler);
|
||||||
|
/// let b = Catcher::new(None, handler);
|
||||||
|
/// assert!(!a.collides_with(&b));
|
||||||
|
/// ```
|
||||||
|
pub fn collides_with(&self, other: &Self) -> bool {
|
||||||
|
self.code == other.code && self.base().segments().eq(other.base().segments())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collide for Route {
|
||||||
|
#[inline(always)]
|
||||||
|
fn collides_with(&self, other: &Route) -> bool {
|
||||||
|
Route::collides_with(&self, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collide for Catcher {
|
||||||
|
#[inline(always)]
|
||||||
fn collides_with(&self, other: &Self) -> bool {
|
fn collides_with(&self, other: &Self) -> bool {
|
||||||
self.code == other.code
|
Catcher::collides_with(&self, other)
|
||||||
&& self.base.path().segments().eq(other.base.path().segments())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,17 +191,17 @@ impl Collide for MediaType {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn formats_collide(route: &Route, other: &Route) -> bool {
|
fn formats_collide(route: &Route, other: &Route) -> bool {
|
||||||
// When matching against the `Accept` header, the client can always provide
|
// If the routes' method doesn't support a payload, then format matching
|
||||||
// a media type that will cause a collision through non-specificity, i.e,
|
// considers the `Accept` header. The client can always provide a media type
|
||||||
// `*/*` matches everything.
|
// that will cause a collision through non-specificity, i.e, `*/*`.
|
||||||
if !route.method.supports_payload() {
|
if !route.method.supports_payload() && !other.method.supports_payload() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When matching against the `Content-Type` header, we'll only consider
|
// Payload supporting methods match against `Content-Type`. We only
|
||||||
// requests as having a `Content-Type` if they're fully specified. If a
|
// consider requests as having a `Content-Type` if they're fully
|
||||||
// route doesn't have a `format`, it accepts all `Content-Type`s. If a
|
// specified. A route without a `format` accepts all `Content-Type`s. A
|
||||||
// request doesn't have a format, it only matches routes without a format.
|
// request without a format only matches routes without a format.
|
||||||
match (route.format.as_ref(), other.format.as_ref()) {
|
match (route.format.as_ref(), other.format.as_ref()) {
|
||||||
(Some(a), Some(b)) => a.collides_with(b),
|
(Some(a), Some(b)) => a.collides_with(b),
|
||||||
_ => true
|
_ => true
|
||||||
|
|
|
@ -4,37 +4,137 @@ use crate::http::Status;
|
||||||
use crate::route::Color;
|
use crate::route::Color;
|
||||||
|
|
||||||
impl Route {
|
impl Route {
|
||||||
/// Determines if this route matches against the given request.
|
/// Returns `true` if `self` matches `request`.
|
||||||
///
|
///
|
||||||
/// This means that:
|
/// A [_match_](Route#routing) occurs when:
|
||||||
///
|
///
|
||||||
/// * The route's method matches that of the incoming request.
|
/// * The route's method matches that of the incoming request.
|
||||||
/// * The route's format (if any) matches that of the incoming request.
|
/// * Either the route has no format _or_:
|
||||||
/// - If route specifies format, it only gets requests for that format.
|
/// - If the route's method supports a payload, the request's
|
||||||
/// - If route doesn't specify format, it gets requests for any format.
|
/// `Content-Type` is [fully specified] and [collides with] the
|
||||||
/// * All static components in the route's path match the corresponding
|
/// route's format.
|
||||||
/// components in the same position in the incoming request.
|
/// - If the route's method does not support a payload, the request
|
||||||
/// * All static components in the route's query string are also in the
|
/// either has no `Accept` header or it [collides with] with the
|
||||||
/// request query string, though in any position. If there is no query
|
/// route's format.
|
||||||
/// in the route, requests with/without queries match.
|
/// * All static segments in the route's URI match the corresponding
|
||||||
#[doc(hidden)]
|
/// components in the same position in the incoming request URI.
|
||||||
pub fn matches(&self, req: &Request<'_>) -> bool {
|
/// * The route URI has no query part _or_ all static segments in the
|
||||||
self.method == req.method()
|
/// route's query string are in the request query string, though in any
|
||||||
&& paths_match(self, req)
|
/// position.
|
||||||
&& queries_match(self, req)
|
///
|
||||||
&& formats_match(self, req)
|
/// [fully specified]: crate::http::MediaType::specificity()
|
||||||
|
/// [collides with]: Route::collides_with()
|
||||||
|
///
|
||||||
|
/// For a request to be routed to a particular route, that route must both
|
||||||
|
/// `match` _and_ have the highest precedence among all matching routes for
|
||||||
|
/// that request. In other words, a `match` is a necessary but insufficient
|
||||||
|
/// condition to determine if a route will handle a particular request.
|
||||||
|
///
|
||||||
|
/// The precedence of a route is determined by its rank. Routes with lower
|
||||||
|
/// ranks have higher precedence. [By default](Route#default-ranking), more
|
||||||
|
/// specific routes are assigned a lower ranking.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rocket::Route;
|
||||||
|
/// use rocket::http::Method;
|
||||||
|
/// # use rocket::local::blocking::Client;
|
||||||
|
/// # use rocket::route::dummy_handler as handler;
|
||||||
|
///
|
||||||
|
/// // This route handles GET requests to `/<hello>`.
|
||||||
|
/// let a = Route::new(Method::Get, "/<hello>", handler);
|
||||||
|
///
|
||||||
|
/// // This route handles GET requests to `/здрасти`.
|
||||||
|
/// let b = Route::new(Method::Get, "/здрасти", handler);
|
||||||
|
///
|
||||||
|
/// # let client = Client::debug(rocket::build()).unwrap();
|
||||||
|
/// // Let's say `request` is `GET /hello`. The request matches only `a`:
|
||||||
|
/// let request = client.get("/hello");
|
||||||
|
/// # let request = request.inner();
|
||||||
|
/// assert!(a.matches(&request));
|
||||||
|
/// assert!(!b.matches(&request));
|
||||||
|
///
|
||||||
|
/// // Now `request` is `GET /здрасти`. It matches both `a` and `b`:
|
||||||
|
/// let request = client.get("/здрасти");
|
||||||
|
/// # let request = request.inner();
|
||||||
|
/// assert!(a.matches(&request));
|
||||||
|
/// assert!(b.matches(&request));
|
||||||
|
///
|
||||||
|
/// // But `b` is more specific, so it has lower rank (higher precedence)
|
||||||
|
/// // by default, so Rocket would route the request to `b`, not `a`.
|
||||||
|
/// assert!(b.rank < a.rank);
|
||||||
|
/// ```
|
||||||
|
pub fn matches(&self, request: &Request<'_>) -> bool {
|
||||||
|
self.method == request.method()
|
||||||
|
&& paths_match(self, request)
|
||||||
|
&& queries_match(self, request)
|
||||||
|
&& formats_match(self, request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Catcher {
|
impl Catcher {
|
||||||
/// Determines if this catcher is responsible for handling the error with
|
/// Returns `true` if `self` matches errors with `status` that occured
|
||||||
/// `status` that occurred during request `req`. A catcher matches if:
|
/// during `request`.
|
||||||
///
|
///
|
||||||
/// * It is a default catcher _or_ has a code of `status`.
|
/// A [_match_](Catcher#routing) between a `Catcher` and a (`Status`,
|
||||||
/// * Its base is a prefix of the normalized/decoded `req.path()`.
|
/// `&Request`) pair occurs when:
|
||||||
pub(crate) fn matches(&self, status: Status, req: &Request<'_>) -> bool {
|
///
|
||||||
|
/// * The catcher has the same [code](Catcher::code) as
|
||||||
|
/// [`status`](Status::code) _or_ is `default`.
|
||||||
|
/// * The catcher's [base](Catcher::base()) is a prefix of the `request`'s
|
||||||
|
/// [normalized](crate::http::uri::Origin#normalization) URI.
|
||||||
|
///
|
||||||
|
/// For an error arising from a request to be routed to a particular
|
||||||
|
/// catcher, that catcher must both `match` _and_ have higher precedence
|
||||||
|
/// than any other catcher that matches. In other words, a `match` is a
|
||||||
|
/// necessary but insufficient condition to determine if a catcher will
|
||||||
|
/// handle a particular error.
|
||||||
|
///
|
||||||
|
/// The precedence of a catcher is determined by:
|
||||||
|
///
|
||||||
|
/// 1. The number of _complete_ segments in the catcher's `base`.
|
||||||
|
/// 2. Whether the catcher is `default` or not.
|
||||||
|
///
|
||||||
|
/// Non-default routes, and routes with more complete segments in their
|
||||||
|
/// base, have higher precedence.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use rocket::Catcher;
|
||||||
|
/// use rocket::http::Status;
|
||||||
|
/// # use rocket::local::blocking::Client;
|
||||||
|
/// # use rocket::catcher::dummy_handler as handler;
|
||||||
|
///
|
||||||
|
/// // This catcher handles 404 errors with a base of `/`.
|
||||||
|
/// let a = Catcher::new(404, handler);
|
||||||
|
///
|
||||||
|
/// // This catcher handles 404 errors with a base of `/bar`.
|
||||||
|
/// let b = a.clone().map_base(|_| format!("/bar")).unwrap();
|
||||||
|
///
|
||||||
|
/// # let client = Client::debug(rocket::build()).unwrap();
|
||||||
|
/// // Let's say `request` is `GET /` that 404s. The error matches only `a`:
|
||||||
|
/// let request = client.get("/");
|
||||||
|
/// # let request = request.inner();
|
||||||
|
/// assert!(a.matches(Status::NotFound, &request));
|
||||||
|
/// assert!(!b.matches(Status::NotFound, &request));
|
||||||
|
///
|
||||||
|
/// // Now `request` is a 404 `GET /bar`. The error matches `a` and `b`:
|
||||||
|
/// let request = client.get("/bar");
|
||||||
|
/// # let request = request.inner();
|
||||||
|
/// assert!(a.matches(Status::NotFound, &request));
|
||||||
|
/// assert!(b.matches(Status::NotFound, &request));
|
||||||
|
///
|
||||||
|
/// // Note that because `b`'s base' has more complete segments that `a's,
|
||||||
|
/// // Rocket would route the error to `b`, not `a`, even though both match.
|
||||||
|
/// let a_count = a.base().segments().filter(|s| !s.is_empty()).count();
|
||||||
|
/// let b_count = b.base().segments().filter(|s| !s.is_empty()).count();
|
||||||
|
/// assert!(b_count > a_count);
|
||||||
|
/// ```
|
||||||
|
pub fn matches(&self, status: Status, request: &Request<'_>) -> bool {
|
||||||
self.code.map_or(true, |code| code == status.code)
|
self.code.map_or(true, |code| code == status.code)
|
||||||
&& self.base.path().segments().prefix_of(req.uri().path().segments())
|
&& self.base().segments().prefix_of(request.uri().path().segments())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue