mirror of https://github.com/rwf2/Rocket.git
Set default route rank using "colorings".
This new system colors paths and queries in one of three ways: 1. `static`, meaning all components are static 2. `partial`, meaning at least one component is dynamic 3. `wild`, meaning all components are dynamic Static paths carry more weight than static queries. The same is true for partial and wild paths. This results in the following default rankings: | path | query | rank | |---------|---------|------| | static | static | -12 | | static | partial | -11 | | static | wild | -10 | | static | none | -9 | | partial | static | -8 | | partial | partial | -7 | | partial | wild | -6 | | partial | none | -5 | | wild | static | -4 | | wild | partial | -3 | | wild | wild | -2 | | wild | none | -1 |
This commit is contained in:
parent
ec1ccc248c
commit
20605dac14
|
@ -1,4 +1,4 @@
|
|||
use super::Route;
|
||||
use super::{Route, uri::Color};
|
||||
|
||||
use crate::http::MediaType;
|
||||
use crate::request::Request;
|
||||
|
@ -45,10 +45,6 @@ impl Route {
|
|||
}
|
||||
|
||||
fn paths_collide(route: &Route, other: &Route) -> bool {
|
||||
if route.uri.metadata.wild_path || other.uri.metadata.wild_path {
|
||||
return true;
|
||||
}
|
||||
|
||||
let a_segments = &route.uri.metadata.path_segs;
|
||||
let b_segments = &other.uri.metadata.path_segs;
|
||||
for (seg_a, seg_b) in a_segments.iter().zip(b_segments.iter()) {
|
||||
|
@ -56,10 +52,12 @@ fn paths_collide(route: &Route, other: &Route) -> bool {
|
|||
return true;
|
||||
}
|
||||
|
||||
if !seg_a.dynamic && !seg_b.dynamic {
|
||||
if seg_a.value != seg_b.value {
|
||||
return false;
|
||||
}
|
||||
if seg_a.dynamic || seg_b.dynamic {
|
||||
continue;
|
||||
}
|
||||
|
||||
if seg_a.value != seg_b.value {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,11 +69,19 @@ fn paths_collide(route: &Route, other: &Route) -> bool {
|
|||
fn paths_match(route: &Route, req: &Request<'_>) -> bool {
|
||||
let route_segments = &route.uri.metadata.path_segs;
|
||||
let req_segments = req.uri().path_segments();
|
||||
if route_segments.len() > req_segments.len() + 1 {
|
||||
|
||||
if route.uri.metadata.trailing_path {
|
||||
// The last route segment can be trailing, which is allowed to be empty.
|
||||
// So we can have one more segment in `route` than in `req` and match.
|
||||
// ok if: req_segments.len() >= route_segments.len() - 1
|
||||
if req_segments.len() + 1 < route_segments.len() {
|
||||
return false;
|
||||
}
|
||||
} else if route_segments.len() != req_segments.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if route.uri.metadata.wild_path {
|
||||
if route.uri.metadata.path_color == Color::Wild {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -89,12 +95,11 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
route_segments.get(req_segments.len()).map_or(false, |s| s.trailing)
|
||||
|| route_segments.len() == req_segments.len()
|
||||
true
|
||||
}
|
||||
|
||||
fn queries_match(route: &Route, req: &Request<'_>) -> bool {
|
||||
if route.uri.metadata.wild_query {
|
||||
if matches!(route.uri.metadata.query_color, None | Some(Color::Wild)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ mod test {
|
|||
fn router_with_routes(routes: &[&'static str]) -> Router {
|
||||
let mut router = Router::new();
|
||||
for route in routes {
|
||||
let route = Route::new(Get, route, dummy);
|
||||
let route = dbg!(Route::new(Get, route, dummy));
|
||||
router.add(route);
|
||||
}
|
||||
|
||||
|
@ -176,6 +176,7 @@ mod test {
|
|||
assert!(rankless_route_collisions(&["//<_..>", "/<_>"]));
|
||||
assert!(rankless_route_collisions(&["///<a>/", "/a/<a..>"]));
|
||||
assert!(rankless_route_collisions(&["///<a..>/", "/a/<a..>"]));
|
||||
assert!(rankless_route_collisions(&["/<a..>", "/hello"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -215,17 +216,26 @@ mod test {
|
|||
assert!(!default_rank_route_collisions(&["/<path..>", "/"]));
|
||||
assert!(!default_rank_route_collisions(&["/<_>/<_>", "/foo/bar"]));
|
||||
assert!(!default_rank_route_collisions(&["/foo/<_>", "/foo/bar"]));
|
||||
|
||||
assert!(!default_rank_route_collisions(&["/<a>/<b>", "/hello/<b>"]));
|
||||
assert!(!default_rank_route_collisions(&["/<a>/<b..>", "/hello/<b>"]));
|
||||
assert!(!default_rank_route_collisions(&["/<a..>", "/hello/<b>"]));
|
||||
assert!(!default_rank_route_collisions(&["/<a..>", "/hello"]));
|
||||
assert!(!default_rank_route_collisions(&["/<a>", "/a/<path..>"]));
|
||||
assert!(!default_rank_route_collisions(&["/a/<b>/c", "/<d>/<c..>"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collision_when_ranked() {
|
||||
assert!(default_rank_route_collisions(&["/<a>", "/a/<path..>"]));
|
||||
assert!(default_rank_route_collisions(&["/a/<b>/<c..>", "/a/<c>"]));
|
||||
assert!(default_rank_route_collisions(&["/<a>/b", "/a/<b>"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collision_when_ranked_query() {
|
||||
assert!(default_rank_route_collisions(&["/a?a=b", "/a?c=d"]));
|
||||
assert!(default_rank_route_collisions(&["/<foo>?a=b", "/<foo>?c=d&<d>"]));
|
||||
assert!(default_rank_route_collisions(&["/a?a=b&<b>", "/a?<c>&c=d"]));
|
||||
assert!(default_rank_route_collisions(&["/a?a=b&<b..>", "/a?<c>&c=d"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -234,6 +244,7 @@ mod test {
|
|||
assert!(!default_rank_route_collisions(&["/hi", "/hi?<c>"]));
|
||||
assert!(!default_rank_route_collisions(&["/hi", "/hi?c"]));
|
||||
assert!(!default_rank_route_collisions(&["/hi?<c>", "/hi?c"]));
|
||||
assert!(!default_rank_route_collisions(&["/<foo>?a=b", "/<foo>?c=d&<d>"]));
|
||||
}
|
||||
|
||||
fn route<'a>(router: &'a Router, method: Method, uri: &'a str) -> Option<&'a Route> {
|
||||
|
|
|
@ -32,53 +32,97 @@ impl Route {
|
|||
///
|
||||
/// # Ranking
|
||||
///
|
||||
/// The route's rank is set so that routes with static paths (no dynamic
|
||||
/// parameters) have lower ranks (higher precedence) than routes with
|
||||
/// dynamic paths, routes with query strings with static segments have lower
|
||||
/// ranks than routes with fully dynamic queries, and routes with queries
|
||||
/// have lower ranks than routes without queries. This default ranking is
|
||||
/// summarized by the table below:
|
||||
/// The default rank prefers static components over dynamic components in
|
||||
/// both paths and queries: the _more_ static a route's path and query are,
|
||||
/// the higher its precedence.
|
||||
///
|
||||
/// | static path | query | rank |
|
||||
/// |-------------|---------------|------|
|
||||
/// | yes | partly static | -6 |
|
||||
/// | yes | fully dynamic | -5 |
|
||||
/// | yes | none | -4 |
|
||||
/// | no | partly static | -3 |
|
||||
/// | no | fully dynamic | -2 |
|
||||
/// | no | none | -1 |
|
||||
/// There are three "colors" to paths and queries:
|
||||
/// 1. `static`, meaning all components are static
|
||||
/// 2. `partial`, meaning at least one component is dynamic
|
||||
/// 3. `wild`, meaning all components are dynamic
|
||||
///
|
||||
/// Static paths carry more weight than static queries. The same is true for
|
||||
/// partial and wild paths. This results in the following default ranking
|
||||
/// table:
|
||||
///
|
||||
/// | path | query | rank |
|
||||
/// |---------|---------|------|
|
||||
/// | static | static | -12 |
|
||||
/// | static | partial | -11 |
|
||||
/// | static | wild | -10 |
|
||||
/// | static | none | -9 |
|
||||
/// | partial | static | -8 |
|
||||
/// | partial | partial | -7 |
|
||||
/// | partial | wild | -6 |
|
||||
/// | partial | none | -5 |
|
||||
/// | wild | static | -4 |
|
||||
/// | wild | partial | -3 |
|
||||
/// | wild | wild | -2 |
|
||||
/// | wild | none | -1 |
|
||||
///
|
||||
/// Note that _lower_ ranks have _higher_ precedence.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::Route;
|
||||
/// use rocket::http::Method;
|
||||
/// # use rocket::{Request, Data};
|
||||
/// # use rocket::handler::{dummy as handler, Outcome, HandlerFuture};
|
||||
/// # use rocket::handler::{dummy as handler};
|
||||
///
|
||||
/// // this is rank -6 (static path, ~static query)
|
||||
/// let route = Route::new(Method::Get, "/foo?bar=baz&<zoo>", handler);
|
||||
/// assert_eq!(route.rank, -6);
|
||||
/// macro_rules! assert_rank {
|
||||
/// ($($uri:expr => $rank:expr,)*) => {$(
|
||||
/// let route = Route::new(Method::Get, $uri, handler);
|
||||
/// assert_eq!(route.rank, $rank);
|
||||
/// )*}
|
||||
/// }
|
||||
///
|
||||
/// // this is rank -5 (static path, fully dynamic query)
|
||||
/// let route = Route::new(Method::Get, "/foo?<zoo..>", handler);
|
||||
/// assert_eq!(route.rank, -5);
|
||||
/// assert_rank! {
|
||||
/// "/?foo" => -12, // static path, static query
|
||||
/// "/foo/bar?a=b&bob" => -12, // static path, static query
|
||||
/// "/?a=b&bob" => -12, // static path, static query
|
||||
///
|
||||
/// // this is a rank -4 route (static path, no query)
|
||||
/// let route = Route::new(Method::Get, "/", handler);
|
||||
/// assert_eq!(route.rank, -4);
|
||||
/// "/?a&<zoo..>" => -11, // static path, partial query
|
||||
/// "/foo?a&<zoo..>" => -11, // static path, partial query
|
||||
/// "/?a&<zoo>" => -11, // static path, partial query
|
||||
///
|
||||
/// // this is a rank -3 route (dynamic path, ~static query)
|
||||
/// let route = Route::new(Method::Get, "/foo/<bar>?blue", handler);
|
||||
/// assert_eq!(route.rank, -3);
|
||||
/// "/?<zoo..>" => -10, // static path, wild query
|
||||
/// "/foo?<zoo..>" => -10, // static path, wild query
|
||||
/// "/foo?<a>&<b>" => -10, // static path, wild query
|
||||
///
|
||||
/// // this is a rank -2 route (dynamic path, fully dynamic query)
|
||||
/// let route = Route::new(Method::Get, "/<bar>?<blue>", handler);
|
||||
/// assert_eq!(route.rank, -2);
|
||||
/// "/" => -9, // static path, no query
|
||||
/// "/foo/bar" => -9, // static path, no query
|
||||
///
|
||||
/// // this is a rank -1 route (dynamic path, no query)
|
||||
/// let route = Route::new(Method::Get, "/<bar>/foo/<baz..>", handler);
|
||||
/// assert_eq!(route.rank, -1);
|
||||
/// "/a/<b>?foo" => -8, // partial path, static query
|
||||
/// "/a/<b..>?foo" => -8, // partial path, static query
|
||||
/// "/<a>/b?foo" => -8, // partial path, static query
|
||||
///
|
||||
/// "/a/<b>?<b>&c" => -7, // partial path, partial query
|
||||
/// "/a/<b..>?a&<c..>" => -7, // partial path, partial query
|
||||
///
|
||||
/// "/a/<b>?<c..>" => -6, // partial path, wild query
|
||||
/// "/a/<b..>?<c>&<d>" => -6, // partial path, wild query
|
||||
/// "/a/<b..>?<c>" => -6, // partial path, wild query
|
||||
///
|
||||
/// "/a/<b>" => -5, // partial path, no query
|
||||
/// "/<a>/b" => -5, // partial path, no query
|
||||
/// "/a/<b..>" => -5, // partial path, no query
|
||||
///
|
||||
/// "/<b>/<c>?foo&bar" => -4, // wild path, static query
|
||||
/// "/<a>/<b..>?foo" => -4, // wild path, static query
|
||||
/// "/<b..>?cat" => -4, // wild path, static query
|
||||
///
|
||||
/// "/<b>/<c>?<foo>&bar" => -3, // wild path, partial query
|
||||
/// "/<a>/<b..>?a&<b..>" => -3, // wild path, partial query
|
||||
/// "/<b..>?cat&<dog>" => -3, // wild path, partial query
|
||||
///
|
||||
/// "/<b>/<c>?<foo>" => -2, // wild path, wild query
|
||||
/// "/<a>/<b..>?<b..>" => -2, // wild path, wild query
|
||||
/// "/<b..>?<c>&<dog>" => -2, // wild path, wild query
|
||||
///
|
||||
/// "/<b>/<c>" => -1, // wild path, no query
|
||||
/// "/<a>/<b..>" => -1, // wild path, no query
|
||||
/// "/<b..>" => -1, // wild path, no query
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
|
@ -96,8 +140,7 @@ impl Route {
|
|||
/// ```rust
|
||||
/// use rocket::Route;
|
||||
/// use rocket::http::Method;
|
||||
/// # use rocket::{Request, Data};
|
||||
/// # use rocket::handler::{dummy as handler, Outcome, HandlerFuture};
|
||||
/// # use rocket::handler::{dummy as handler};
|
||||
///
|
||||
/// // this is a rank 1 route matching requests to `GET /`
|
||||
/// let index = Route::ranked(1, Method::Get, "/", handler);
|
||||
|
|
|
@ -67,7 +67,17 @@ pub struct RouteUri<'a> {
|
|||
pub(crate) metadata: Metadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Color {
|
||||
/// Fully static: no dynamic components.
|
||||
Static = 3,
|
||||
/// Partially static/dynamic: some, but not all, dynamic components.
|
||||
Partial = 2,
|
||||
/// Fully dynamic: no static components.
|
||||
Wild = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Metadata {
|
||||
/// Segments in the base.
|
||||
pub base_segs: Vec<Segment>,
|
||||
|
@ -77,14 +87,12 @@ pub(crate) struct Metadata {
|
|||
pub query_segs: Vec<Segment>,
|
||||
/// `(name, value)` of the query segments that are static.
|
||||
pub static_query_fields: Vec<(String, String)>,
|
||||
/// Whether the path is completely static.
|
||||
pub static_path: bool,
|
||||
/// Whether the path is completely dynamic.
|
||||
pub wild_path: bool,
|
||||
/// The "color" of the route path.
|
||||
pub path_color: Color,
|
||||
/// The "color" of the route query, if there is query.
|
||||
pub query_color: Option<Color>,
|
||||
/// Whether the path has a `<trailing..>` parameter.
|
||||
pub trailing_path: bool,
|
||||
/// Whether the query is completely dynamic.
|
||||
pub wild_query: bool,
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, uri::Error<'static>>;
|
||||
|
@ -206,17 +214,28 @@ impl<'a> RouteUri<'a> {
|
|||
/// The route's default rank is determined based on the presence or absence
|
||||
/// of static and dynamic paths and queries. See the documentation for
|
||||
/// [`Route::new`][`crate::Route::new`] for a table summarizing the exact default ranks.
|
||||
///
|
||||
/// | path | query | rank |
|
||||
/// |---------|---------|------|
|
||||
/// | static | static | -12 |
|
||||
/// | static | partial | -11 |
|
||||
/// | static | wild | -10 |
|
||||
/// | static | none | -9 |
|
||||
/// | partial | static | -8 |
|
||||
/// | partial | partial | -7 |
|
||||
/// | partial | wild | -6 |
|
||||
/// | partial | none | -5 |
|
||||
/// | wild | static | -4 |
|
||||
/// | wild | partial | -3 |
|
||||
/// | wild | wild | -2 |
|
||||
/// | wild | none | -1 |
|
||||
pub(crate) fn default_rank(&self) -> isize {
|
||||
let static_path = self.metadata.static_path;
|
||||
let wild_query = self.query().map(|_| self.metadata.wild_query);
|
||||
match (static_path, wild_query) {
|
||||
(true, Some(false)) => -6, // static path, partly static query
|
||||
(true, Some(true)) => -5, // static path, fully dynamic query
|
||||
(true, None) => -4, // static path, no query
|
||||
(false, Some(false)) => -3, // dynamic path, partly static query
|
||||
(false, Some(true)) => -2, // dynamic path, fully dynamic query
|
||||
(false, None) => -1, // dynamic path, no query
|
||||
}
|
||||
let raw_path_weight = self.metadata.path_color as u8;
|
||||
let raw_query_weight = self.metadata.query_color.map_or(0, |c| c as u8);
|
||||
let raw_weight = (raw_path_weight << 2) | raw_query_weight;
|
||||
|
||||
// We subtract `3` because `raw_path` is never `0`: 0b0100 = 4 - 3 = 1.
|
||||
-((raw_weight as isize) - 3)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,19 +253,34 @@ impl Metadata {
|
|||
.map(Segment::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let static_query_fields = query_segs.iter().filter(|s| !s.dynamic)
|
||||
.map(|s| ValueField::parse(&s.value))
|
||||
.map(|f| (f.name.source().to_string(), f.value.to_string()))
|
||||
.collect();
|
||||
|
||||
let static_path = path_segs.iter().all(|s| !s.dynamic);
|
||||
let wild_path = !path_segs.is_empty() && path_segs.iter().all(|s| s.dynamic);
|
||||
let path_color = match (static_path, wild_path) {
|
||||
(true, _) => Color::Static,
|
||||
(_, true) => Color::Wild,
|
||||
(_, _) => Color::Partial
|
||||
};
|
||||
|
||||
let query_color = (!query_segs.is_empty()).then(|| {
|
||||
let static_query = query_segs.iter().all(|s| !s.dynamic);
|
||||
let wild_query = query_segs.iter().all(|s| s.dynamic);
|
||||
match (static_query, wild_query) {
|
||||
(true, _) => Color::Static,
|
||||
(_, true) => Color::Wild,
|
||||
(_, _) => Color::Partial
|
||||
}
|
||||
});
|
||||
|
||||
let trailing_path = path_segs.last().map_or(false, |p| p.trailing);
|
||||
|
||||
Metadata {
|
||||
static_path: path_segs.iter().all(|s| !s.dynamic),
|
||||
wild_path: path_segs.iter().all(|s| s.dynamic)
|
||||
&& path_segs.last().map_or(false, |p| p.trailing),
|
||||
trailing_path: path_segs.last().map_or(false, |p| p.trailing),
|
||||
wild_query: query_segs.iter().all(|s| s.dynamic),
|
||||
static_query_fields: query_segs.iter().filter(|s| !s.dynamic)
|
||||
.map(|s| ValueField::parse(&s.value))
|
||||
.map(|f| (f.name.source().to_string(), f.value.to_string()))
|
||||
.collect(),
|
||||
path_segs,
|
||||
query_segs,
|
||||
base_segs,
|
||||
static_query_fields, path_color, query_color, trailing_path,
|
||||
path_segs, query_segs, base_segs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -271,6 +305,7 @@ impl fmt::Debug for RouteUri<'_> {
|
|||
.field("base", &self.base)
|
||||
.field("unmounted_origin", &self.unmounted_origin)
|
||||
.field("origin", &self.origin)
|
||||
.field("metadata", &self.metadata)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -243,20 +243,35 @@ parameter resolves this collision.
|
|||
|
||||
### Default Ranking
|
||||
|
||||
If a rank is not explicitly specified, Rocket assigns a default ranking. By
|
||||
default, routes with static paths and query strings have lower ranks (higher
|
||||
precedence) while routes with dynamic paths and without query strings have
|
||||
higher ranks (lower precedence). The table below describes the default ranking
|
||||
of a route given its properties.
|
||||
If a rank is not explicitly specified, Rocket assigns a default rank. The
|
||||
default rank prefers static segments over dynamic segments in both paths and
|
||||
queries: the _more_ static a route's path and query are, the higher its
|
||||
precedence.
|
||||
|
||||
| static path | query | rank | example |
|
||||
|-------------|---------------|------|---------------------|
|
||||
| yes | partly static | -6 | `/hello?world=true` |
|
||||
| yes | fully dynamic | -5 | `/hello/?<world>` |
|
||||
| yes | none | -4 | `/hello` |
|
||||
| no | partly static | -3 | `/<hi>?world=true` |
|
||||
| no | fully dynamic | -2 | `/<hi>?<world>` |
|
||||
| no | none | -1 | `/<hi>` |
|
||||
There are three "colors" to paths and queries:
|
||||
1. `static`, meaning all components are static
|
||||
2. `partial`, meaning at least one component is dynamic
|
||||
3. `wild`, meaning all components are dynamic
|
||||
|
||||
Static paths carry more weight than static queries. The same is true for partial
|
||||
and wild paths. This results in the following default ranking table:
|
||||
|
||||
| path color | query color | default rank |
|
||||
|------------|-------------|--------------|
|
||||
| static | static | -12 |
|
||||
| static | partial | -11 |
|
||||
| static | wild | -10 |
|
||||
| static | none | -9 |
|
||||
| partial | static | -8 |
|
||||
| partial | partial | -7 |
|
||||
| partial | wild | -6 |
|
||||
| partial | none | -5 |
|
||||
| wild | static | -4 |
|
||||
| wild | partial | -3 |
|
||||
| wild | wild | -2 |
|
||||
| wild | none | -1 |
|
||||
|
||||
Recall that _lower_ ranks have _higher_ precedence.
|
||||
|
||||
## Request Guards
|
||||
|
||||
|
|
Loading…
Reference in New Issue