Introduce 'RouteUri'.

Co-authored-by: Ben Sully <ben@bsull.io>
This commit is contained in:
Sergio Benitez 2021-03-17 00:46:33 -07:00
parent 487485d108
commit 2463637d51
9 changed files with 273 additions and 202 deletions

View File

@ -95,7 +95,7 @@ fn bench_simple_routing(b: &mut Bencher) {
// Hold all of the requests we're going to make during the benchmark.
let mut requests = vec![];
for route in client.rocket().routes() {
let request = client.req(route.method, route.uri.path().as_str());
let request = client.req(route.method, route.uri.path());
requests.push(request);
}

View File

@ -212,30 +212,3 @@ impl Drop for Error {
}
}
}
use crate::http::uri;
use crate::http::ext::IntoOwned;
/// Error returned by [`Route::map_base()`] on invalid URIs.
#[derive(Debug)]
pub enum RouteUriError {
/// The route URI is not a valid URI.
Uri(uri::Error<'static>),
/// The base (mount point) contains dynamic segments.
DynamicBase,
}
impl<'a> From<uri::Error<'a>> for RouteUriError {
fn from(error: uri::Error<'a>) -> Self {
RouteUriError::Uri(error.into_owned())
}
}
impl fmt::Display for RouteUriError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RouteUriError::DynamicBase => write!(f, "Mount point contains dynamic parameters."),
RouteUriError::Uri(error) => write!(f, "Malformed URI: {}", error)
}
}
}

View File

@ -792,7 +792,7 @@ impl<'r> Request<'r> {
#[inline]
pub fn routed_segments(&self, n: RangeFrom<usize>) -> Segments<'_> {
let mount_segments = self.route()
.map(|r| r.base.path_segments().len())
.map(|r| r.uri.metadata.base_segs.len())
.unwrap_or(0);
self.uri().path_segments().skip(mount_segments + n.start)

View File

@ -222,16 +222,16 @@ impl Rocket {
Paint::magenta(":"));
for route in routes.into() {
let old_route = route.clone();
let route = route.map_base(|old| format!("{}{}", base, old))
let mounted_route = route.clone()
.map_base(|old| format!("{}{}", base, old))
.unwrap_or_else(|e| {
error_!("Route `{}` has a malformed URI.", old_route);
error_!("Route `{}` has a malformed URI.", route);
error_!("{}", e);
panic!("Invalid route URI.");
});
info_!("{}", route);
self.router.add(route);
info_!("{}", mounted_route);
self.router.add(mounted_route);
}
self

View File

@ -46,12 +46,12 @@ impl Route {
}
fn paths_collide(route: &Route, other: &Route) -> bool {
if route.metadata.wild_path || other.metadata.wild_path {
if route.uri.metadata.wild_path || other.uri.metadata.wild_path {
return true;
}
let a_segments = &route.metadata.path_segs;
let b_segments = &other.metadata.path_segs;
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()) {
if seg_a.trailing || seg_b.trailing {
return true;
@ -70,13 +70,13 @@ fn paths_collide(route: &Route, other: &Route) -> bool {
}
fn paths_match(route: &Route, req: &Request<'_>) -> bool {
let route_segments = &route.metadata.path_segs;
let route_segments = &route.uri.metadata.path_segs;
let req_segments = req.routed_segments(0..);
if route_segments.len() > req_segments.len() + 1 {
return false;
}
if route.metadata.wild_path {
if route.uri.metadata.wild_path {
return true;
}
@ -95,11 +95,11 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool {
}
fn queries_match(route: &Route, req: &Request<'_>) -> bool {
if route.metadata.wild_query {
if route.uri.metadata.wild_query {
return true;
}
let route_query_fields = route.metadata.static_query_fields.iter()
let route_query_fields = route.uri.metadata.static_query_fields.iter()
.map(|(k, v)| (k.as_str(), v.as_str()));
for route_seg in route_query_fields {
@ -482,7 +482,7 @@ mod tests {
fn req_route_path_match(a: &'static str, b: &'static str) -> bool {
let rocket = Rocket::custom(Config::default());
let req = Request::new(&rocket, Get, Origin::parse(a).expect("valid URI"));
let route = Route::ranked(0, Get, b.to_string(), dummy);
let route = Route::ranked(0, Get, b, dummy);
route.matches(&req)
}

View File

@ -1,6 +1,7 @@
mod collider;
mod route;
mod segment;
mod uri;
use std::collections::HashMap;
@ -10,6 +11,7 @@ use crate::handler::dummy;
pub use self::route::Route;
pub use self::segment::Segment;
pub use self::uri::RouteUri;
// type Selector = (Method, usize);
type Selector = Method;
@ -108,7 +110,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.to_string(), dummy);
let route = Route::new(Get, route, dummy);
router.add(route);
}
@ -118,7 +120,7 @@ mod test {
fn router_with_ranked_routes(routes: &[(isize, &'static str)]) -> Router {
let mut router = Router::new();
for &(rank, route) in routes {
let route = Route::ranked(rank, Get, route.to_string(), dummy);
let route = Route::ranked(rank, Get, route, dummy);
router.add(route);
}
@ -128,7 +130,7 @@ mod test {
fn router_with_rankless_routes(routes: &[&'static str]) -> Router {
let mut router = Router::new();
for route in routes {
let route = Route::ranked(0, Get, route.to_string(), dummy);
let route = Route::ranked(0, Get, route, dummy);
router.add(route);
}
@ -300,9 +302,9 @@ mod test {
assert!(route(&router, Get, "/jdlk/asdij").is_some());
let mut router = Router::new();
router.add(Route::new(Put, "/hello".to_string(), dummy));
router.add(Route::new(Post, "/hello".to_string(), dummy));
router.add(Route::new(Delete, "/hello".to_string(), dummy));
router.add(Route::new(Put, "/hello", dummy));
router.add(Route::new(Post, "/hello", dummy));
router.add(Route::new(Delete, "/hello", dummy));
assert!(route(&router, Put, "/hello").is_some());
assert!(route(&router, Post, "/hello").is_some());
assert!(route(&router, Delete, "/hello").is_some());

View File

@ -1,4 +1,4 @@
use std::fmt::{self, Display};
use std::fmt;
use std::convert::From;
use std::borrow::Cow;
@ -6,12 +6,8 @@ use yansi::Paint;
use crate::codegen::StaticRouteInfo;
use crate::handler::Handler;
use crate::http::{Method, MediaType};
use crate::error::RouteUriError;
use crate::http::ext::IntoOwned;
use crate::http::uri::Origin;
use crate::router::Segment;
use crate::form::ValueField;
use crate::http::{uri, Method, MediaType};
use crate::router::RouteUri;
/// A route: a method, its handler, path, rank, and format/media type.
#[derive(Clone)]
@ -22,48 +18,12 @@ pub struct Route {
pub method: Method,
/// The function that should be called when the route matches.
pub handler: Box<dyn Handler>,
/// The base mount point of this `Route`.
pub base: Origin<'static>,
/// The path of this `Route` in Rocket's route format.
pub(crate) path: Origin<'static>,
/// The complete URI (in Rocket's route format) that should be matched
/// against. This is `base` + `path`.
pub uri: Origin<'static>,
/// The route URI.
pub uri: RouteUri<'static>,
/// The rank of this route. Lower ranks have higher priorities.
pub rank: isize,
/// The media type this route matches against, if any.
pub format: Option<MediaType>,
/// Cached metadata that aids in routing later.
pub(crate) metadata: Metadata,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct Metadata {
pub path_segs: Vec<Segment>,
pub query_segs: Vec<Segment>,
pub static_query_fields: Vec<(String, String)>,
pub static_path: bool,
pub wild_path: bool,
pub trailing_path: bool,
pub wild_query: bool,
}
#[inline(always)]
fn default_rank(route: &Route) -> isize {
let static_path = route.metadata.static_path;
let wild_query = route.uri.query().map(|_| route.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
}
}
fn panic<U: Display, E: Display, T>(uri: U, e: E) -> T {
panic!("invalid URI '{}' used to construct route: {}", uri, e)
}
impl Route {
@ -124,12 +84,8 @@ impl Route {
/// # Panics
///
/// Panics if `path` is not a valid origin URI or Rocket route URI.
pub fn new<S, H>(method: Method, path: S, handler: H) -> Route
where S: AsRef<str>, H: Handler
{
let mut route = Route::ranked(0, method, path, handler);
route.rank = default_rank(&route);
route
pub fn new<H: Handler>(method: Method, uri: &str, handler: H) -> Route {
Route::ranked(None, method, uri, handler)
}
/// Creates a new route with the given rank, method, path, and handler with
@ -150,108 +106,27 @@ impl Route {
/// # Panics
///
/// Panics if `path` is not a valid origin URI or Rocket route URI.
pub fn ranked<S, H>(rank: isize, method: Method, path: S, handler: H) -> Route
where S: AsRef<str>, H: Handler + 'static
pub fn ranked<H, R>(rank: R, method: Method, uri: &str, handler: H) -> Route
where H: Handler + 'static, R: Into<Option<isize>>,
{
let path = path.as_ref();
let route_path = Origin::parse_route(path)
.unwrap_or_else(|e| panic(path, e))
.into_normalized()
.into_owned();
let mut route = Route {
path: route_path.clone(),
uri: route_path,
let uri = RouteUri::new("/", uri);
let rank = rank.into().unwrap_or_else(|| uri.default_rank());
Route {
name: None,
format: None,
base: Origin::dummy(),
handler: Box::new(handler),
metadata: Metadata::default(),
method, rank,
};
route.update_metadata();
route
}
fn metadata(&self) -> Metadata {
let path_segs = self.uri.raw_path_segments()
.map(Segment::from)
.collect::<Vec<_>>();
let query_segs = self.uri.raw_query_segments()
.map(Segment::from)
.collect::<Vec<_>>();
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,
rank, uri, method,
}
}
/// Updates the cached routing metadata. MUST be called whenver the route's
/// URI is set or changes.
fn update_metadata(&mut self) {
self.metadata = self.metadata();
}
/// Retrieves the path of the base mount point of this route as an `&str`.
///
/// # Example
///
/// ```rust
/// use rocket::Route;
/// use rocket::http::Method;
/// # use rocket::handler::dummy as handler;
///
/// let mut index = Route::new(Method::Get, "/", handler);
/// assert_eq!(index.base(), "/");
/// assert_eq!(index.base.path(), "/");
/// ```
#[inline]
pub fn base(&self) -> &str {
// This is ~ok as the route path is assumed to be percent decoded.
self.base.path().as_str()
}
/// Retrieves this route's path.
///
/// # Example
///
/// ```rust
/// use rocket::Route;
/// use rocket::http::Method;
/// # use rocket::handler::dummy as handler;
///
/// let index = Route::new(Method::Get, "/foo/bar?a=1", handler);
/// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap();
/// assert_eq!(index.uri.path(), "/boo/foo/bar");
/// assert_eq!(index.uri.query().unwrap(), "a=1");
/// assert_eq!(index.base(), "/boo");
/// assert_eq!(index.path().path(), "/foo/bar");
/// assert_eq!(index.path().query().unwrap(), "a=1");
/// ```
#[inline]
pub fn path(&self) -> &Origin<'_> {
&self.path
}
/// Maps the `base` of this route using `mapper`, returning a new `Route`
/// with the returned base.
///
/// `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 if the base produced by `mapper` is
/// not a valid origin URI.
/// a query, it is ignored. Returns an error if the base produced by
/// `mapper` is not a valid origin URI.
///
/// # Example
///
@ -270,15 +145,11 @@ impl Route {
/// assert_eq!(index.path().path(), "/foo/bar");
/// assert_eq!(index.uri.path(), "/boo/foo/bar");
/// ```
pub fn map_base<'a, F>(mut self, mapper: F) -> Result<Self, RouteUriError>
where F: FnOnce(Origin<'static>) -> String
pub fn map_base<'a, F>(mut self, mapper: F) -> Result<Self, uri::Error<'static>>
where F: FnOnce(uri::Origin<'a>) -> String
{
self.base = Origin::parse_owned(mapper(self.base))?.into_normalized();
self.base.clear_query();
let new_uri = format!("{}{}", self.base, self.path);
self.uri = Origin::parse_route(&new_uri)?.into_owned().into_normalized();
self.update_metadata();
let base = mapper(self.uri.base);
self.uri = RouteUri::try_new(&base, &self.uri.unmounted_origin.to_string())?;
Ok(self)
}
}
@ -290,11 +161,11 @@ impl fmt::Display for Route {
}
write!(f, "{} ", Paint::green(&self.method))?;
if self.base.path() != "/" {
write!(f, "{}", Paint::blue(&self.base).underline())?;
if self.uri.base() != "/" {
write!(f, "{}", Paint::blue(self.uri.base()).underline())?;
}
write!(f, "{}", Paint::blue(&self.path))?;
write!(f, "{}", Paint::blue(&self.uri.unmounted_origin))?;
if self.rank > 1 {
write!(f, " [{}]", Paint::default(&self.rank).bold())?;
@ -313,11 +184,9 @@ impl fmt::Debug for Route {
f.debug_struct("Route")
.field("name", &self.name)
.field("method", &self.method)
.field("base", &self.base)
.field("uri", &self.uri)
.field("rank", &self.rank)
.field("format", &self.format)
.field("metadata", &self.metadata)
.finish()
}
}

227
core/lib/src/router/uri.rs Normal file
View File

@ -0,0 +1,227 @@
use std::fmt;
use std::borrow::Cow;
use crate::http::uri::{self, Origin};
use crate::http::ext::IntoOwned;
use crate::router::Segment;
use crate::form::ValueField;
#[derive(Clone)]
pub struct RouteUri<'a> {
/// The source string for this URI.
source: Cow<'a, str>,
/// The mount point of this `Route`.
pub base: Origin<'a>,
/// The URI _without_ the `base`.
pub unmounted_origin: Origin<'a>,
/// The URI _with_ the base. This is the canoncical route URI.
pub origin: Origin<'a>,
/// Cached metadata about this URI.
pub(crate) metadata: Metadata,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct Metadata {
/// Segments in the base.
pub base_segs: Vec<Segment>,
/// Segments in the path, including base.
pub path_segs: Vec<Segment>,
/// Segments in the query.
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,
/// 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>>;
impl<'a> RouteUri<'a> {
/// Create a new `RouteUri`. Don't panic.
pub(crate) fn try_new(base: &str, uri: &str) -> Result<RouteUri<'static>> {
let mut base = Origin::parse(base)
.map_err(|e| e.into_owned())?
.into_normalized()
.into_owned();
base.clear_query();
let unmounted_origin = Origin::parse_route(uri)
.map_err(|e| e.into_owned())?
.into_normalized()
.into_owned();
let origin = Origin::parse_route(&format!("{}/{}", base, unmounted_origin))
.map_err(|e| e.into_owned())?
.into_normalized()
.into_owned();
let source = origin.to_string().into();
let metadata = Metadata::from(&base, &origin);
Ok(RouteUri { source, unmounted_origin, base, origin, metadata })
}
/// Create a new `RouteUri`. Panic.
pub(crate) fn new(base: &str, uri: &str) -> RouteUri<'static> {
Self::try_new(base, uri).expect("FIXME MSG")
}
/// The path of the base mount point of this route URI as an `&str`.
///
/// # Example
///
/// ```rust
/// use rocket::RouteUri;
///
/// let index = RouteUri::new("/foo/bar?a=1");
/// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap();
/// assert_eq!(index.uri(), "/boo/foo/bar");
/// assert_eq!(index.base(), "/boo");
/// assert_eq!(index.path(), "/foo/bar");
/// assert_eq!(index.query().unwrap(), "a=1");
/// assert_eq!(index.as_str(), "/boo/foo/bar?a=1");
/// ```
#[inline(always)]
pub fn base(&self) -> &str {
self.base.path().as_str()
}
/// The path part of this route URI as an `&str`.
///
/// # Example
///
/// ```rust
/// use rocket::RouteUri;
///
/// let index = RouteUri::new("/foo/bar?a=1");
/// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap();
/// assert_eq!(index.uri(), "/boo/foo/bar");
/// assert_eq!(index.base(), "/boo");
/// assert_eq!(index.path(), "/foo/bar");
/// assert_eq!(index.query().unwrap(), "a=1");
/// assert_eq!(index.as_str(), "/boo/foo/bar?a=1");
/// ```
#[inline(always)]
pub fn path(&self) -> &str {
self.origin.path().as_str()
}
/// The query part of this route URI as an `Option<&str>`.
///
/// # Example
///
/// ```rust
/// use rocket::RouteUri;
///
/// let index = RouteUri::new("/foo/bar?a=1");
/// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap();
/// assert_eq!(index.uri(), "/boo/foo/bar");
/// assert_eq!(index.base(), "/boo");
/// assert_eq!(index.path(), "/foo/bar");
/// assert_eq!(index.query().unwrap(), "a=1");
/// assert_eq!(index.as_str(), "/boo/foo/bar?a=1");
/// ```
#[inline(always)]
pub fn query(&self) -> Option<&str> {
self.origin.query().map(|q| q.as_str())
}
/// The full URI as an `&str`.
///
/// # Example
///
/// ```rust
/// use rocket::RouteUri;
///
/// let index = RouteUri::new("/foo/bar?a=1");
/// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap();
/// assert_eq!(index.uri(), "/boo/foo/bar");
/// assert_eq!(index.base(), "/boo");
/// assert_eq!(index.path(), "/foo/bar");
/// assert_eq!(index.query().unwrap(), "a=1");
/// assert_eq!(index.as_str(), "/boo/foo/bar?a=1");
/// ```
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.source
}
#[inline(always)]
pub fn as_origin(&self) -> &Origin<'a> {
&self.origin
}
pub 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
}
}
}
impl Metadata {
fn from(base: &Origin<'_>, origin: &Origin<'_>) -> Self {
let base_segs = base.raw_path_segments()
.map(Segment::from)
.collect::<Vec<_>>();
let path_segs = origin.raw_path_segments()
.map(Segment::from)
.collect::<Vec<_>>();
let query_segs = origin.raw_query_segments()
.map(Segment::from)
.collect::<Vec<_>>();
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,
}
}
}
impl<'a> std::ops::Deref for RouteUri<'a> {
type Target = Origin<'a>;
fn deref(&self) -> &Self::Target {
self.as_origin()
}
}
impl fmt::Display for RouteUri<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.origin.fmt(f)
}
}
impl fmt::Debug for RouteUri<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RouteUri")
.field("base", &self.base)
.field("uri", &self.as_origin())
.finish()
}
}

View File

@ -6,7 +6,7 @@ use rocket::Route;
#[get("/<path..>")]
fn files(route: &Route, path: PathBuf) -> String {
Path::new(route.base()).join(path).normalized_str().to_string()
Path::new(route.uri.base()).join(path).normalized_str().to_string()
}
mod route_guard_tests {