mirror of https://github.com/rwf2/Rocket.git
332 lines
11 KiB
Rust
332 lines
11 KiB
Rust
use std::fmt::{self, Display};
|
|
use std::convert::From;
|
|
|
|
use yansi::Color::*;
|
|
|
|
use codegen::StaticRouteInfo;
|
|
use handler::Handler;
|
|
use http::{Method, MediaType};
|
|
use http::route::{RouteSegment, Kind};
|
|
use error::RouteUriError;
|
|
use http::ext::IntoOwned;
|
|
use http::uri::Origin;
|
|
|
|
/// A route: a method, its handler, path, rank, and format/media type.
|
|
#[derive(Clone)]
|
|
pub struct Route {
|
|
/// The name of this route, if one was given.
|
|
pub name: Option<&'static str>,
|
|
/// The method this route matches against.
|
|
pub method: Method,
|
|
/// The function that should be called when the route matches.
|
|
pub handler: Box<Handler>,
|
|
/// The base mount point of this `Route`.
|
|
pub base: Origin<'static>,
|
|
/// The uri (in Rocket's route format) that should be matched against. This
|
|
/// URI already includes the base mount point.
|
|
pub uri: Origin<'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.
|
|
crate metadata: Metadata
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
crate struct Metadata {
|
|
crate path_segments: Vec<RouteSegment<'static>>,
|
|
crate query_segments: Option<Vec<RouteSegment<'static>>>,
|
|
crate fully_dynamic_query: bool,
|
|
}
|
|
|
|
impl Metadata {
|
|
fn from(route: &Route) -> Result<Metadata, RouteUriError> {
|
|
let path_segments = RouteSegment::parse_path(&route.uri)
|
|
.map(|res| res.map(|s| s.into_owned()))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let (query_segments, dyn) = match RouteSegment::parse_query(&route.uri) {
|
|
Some(results) => {
|
|
let segments = results.map(|res| res.map(|s| s.into_owned()))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let dynamic = !segments.iter().any(|s| s.kind == Kind::Static);
|
|
|
|
(Some(segments), dynamic)
|
|
}
|
|
None => (None, true)
|
|
};
|
|
|
|
Ok(Metadata { path_segments, query_segments, fully_dynamic_query: dyn })
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn default_rank(route: &Route) -> isize {
|
|
let static_path = route.metadata.path_segments.iter().all(|s| s.kind == Kind::Static);
|
|
let partly_static_query = route.uri.query().map(|_| !route.metadata.fully_dynamic_query);
|
|
match (static_path, partly_static_query) {
|
|
(true, Some(true)) => -6, // static path, partly static query
|
|
(true, Some(false)) => -5, // static path, fully dynamic query
|
|
(true, None) => -4, // static path, no query
|
|
(false, Some(true)) => -3, // dynamic path, partly static query
|
|
(false, Some(false)) => -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 {
|
|
/// Creates a new route with the given method, path, and handler with a base
|
|
/// of `/`.
|
|
///
|
|
/// # Ranking
|
|
///
|
|
/// The route's rank is set so that routes with static paths (no dynamic
|
|
/// parameters) are ranked higher than routes with dynamic paths, routes
|
|
/// with query strings with static segments are ranked higher than routes
|
|
/// with fully dynamic queries, and routes with queries are ranked higher
|
|
/// than routes without queries. This default ranking is summarized by the
|
|
/// table below:
|
|
///
|
|
/// | 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 |
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use rocket::Route;
|
|
/// use rocket::http::Method;
|
|
/// # use rocket::{Request, Data};
|
|
/// # use rocket::handler::Outcome;
|
|
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
|
/// # Outcome::from(request, "Hello, world!")
|
|
/// # }
|
|
///
|
|
/// // this is rank -6 (static path, ~static query)
|
|
/// let route = Route::new(Method::Get, "/foo?bar=baz&<zoo>", handler);
|
|
/// assert_eq!(route.rank, -6);
|
|
///
|
|
/// // this is rank -5 (static path, fully dynamic query)
|
|
/// let route = Route::new(Method::Get, "/foo?<zoo..>", handler);
|
|
/// assert_eq!(route.rank, -5);
|
|
///
|
|
/// // this is a rank -4 route (static path, no query)
|
|
/// let route = Route::new(Method::Get, "/", handler);
|
|
/// assert_eq!(route.rank, -4);
|
|
///
|
|
/// // 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);
|
|
///
|
|
/// // 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);
|
|
///
|
|
/// // 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);
|
|
/// ```
|
|
///
|
|
/// # 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 + 'static
|
|
{
|
|
let mut route = Route::ranked(0, method, path, handler);
|
|
route.rank = default_rank(&route);
|
|
route
|
|
}
|
|
|
|
/// Creates a new route with the given rank, method, path, and handler with
|
|
/// a base of `/`.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use rocket::Route;
|
|
/// use rocket::http::Method;
|
|
/// # use rocket::{Request, Data};
|
|
/// # use rocket::handler::Outcome;
|
|
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
|
/// # Outcome::from(request, "Hello, world!")
|
|
/// # }
|
|
///
|
|
/// // this is a rank 1 route matching requests to `GET /`
|
|
/// let index = Route::ranked(1, Method::Get, "/", handler);
|
|
/// ```
|
|
///
|
|
/// # 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
|
|
{
|
|
let path = path.as_ref();
|
|
let uri = Origin::parse_route(path)
|
|
.unwrap_or_else(|e| panic(path, e))
|
|
.to_normalized()
|
|
.into_owned();
|
|
|
|
let mut route = Route {
|
|
name: None,
|
|
format: None,
|
|
base: Origin::dummy(),
|
|
handler: Box::new(handler),
|
|
metadata: Metadata::default(),
|
|
method, rank, uri
|
|
};
|
|
|
|
route.update_metadata().unwrap_or_else(|e| panic(path, e));
|
|
route
|
|
}
|
|
|
|
/// Updates the cached routing metadata. MUST be called whenver the route's
|
|
/// URI is set or changes.
|
|
fn update_metadata(&mut self) -> Result<(), RouteUriError> {
|
|
let new_metadata = Metadata::from(&*self)?;
|
|
self.metadata = new_metadata;
|
|
Ok(())
|
|
}
|
|
|
|
/// 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::{Request, Data};
|
|
/// # use rocket::handler::Outcome;
|
|
/// #
|
|
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
|
/// # Outcome::from(request, "Hello, world!")
|
|
/// # }
|
|
///
|
|
/// let mut index = Route::new(Method::Get, "/", handler);
|
|
/// assert_eq!(index.base(), "/");
|
|
/// assert_eq!(index.base.path(), "/");
|
|
/// ```
|
|
#[inline]
|
|
pub fn base(&self) -> &str {
|
|
self.base.path()
|
|
}
|
|
|
|
/// Sets the base mount point of the route to `base` and sets the path to
|
|
/// `path`. The `path` should _not_ contains the `base` mount point. If
|
|
/// `base` contains a query, it is ignored. Note that `self.uri` will
|
|
/// include the new `base` after this method is called.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if any of the following occur:
|
|
///
|
|
/// * The base mount point contains dynamic parameters.
|
|
/// * The base mount point or path contain encoded characters.
|
|
/// * The path is not a valid Rocket route URI.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use rocket::Route;
|
|
/// use rocket::http::{Method, uri::Origin};
|
|
/// # use rocket::{Request, Data};
|
|
/// # use rocket::handler::Outcome;
|
|
/// #
|
|
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
|
/// # Outcome::from(request, "Hello, world!")
|
|
/// # }
|
|
///
|
|
/// let mut index = Route::new(Method::Get, "/", handler);
|
|
/// assert_eq!(index.base(), "/");
|
|
/// assert_eq!(index.base.path(), "/");
|
|
///
|
|
/// let new_base = Origin::parse("/greeting").unwrap();
|
|
/// let new_uri = Origin::parse("/hi").unwrap();
|
|
/// index.set_uri(new_base, new_uri);
|
|
/// assert_eq!(index.base(), "/greeting");
|
|
/// assert_eq!(index.uri.path(), "/greeting/hi");
|
|
/// ```
|
|
pub fn set_uri<'a>(
|
|
&mut self,
|
|
mut base: Origin<'a>,
|
|
path: Origin<'a>
|
|
) -> Result<(), RouteUriError> {
|
|
base.clear_query();
|
|
for segment in RouteSegment::parse_path(&base) {
|
|
if segment?.kind != Kind::Static {
|
|
return Err(RouteUriError::DynamicBase);
|
|
}
|
|
}
|
|
|
|
let complete_uri = format!("{}/{}", base, path);
|
|
let uri = Origin::parse_route(&complete_uri)?;
|
|
self.base = base.to_normalized().into_owned();
|
|
self.uri = uri.to_normalized().into_owned();
|
|
self.update_metadata()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Route {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{} {}", Green.paint(&self.method), Blue.paint(&self.uri))?;
|
|
|
|
if self.rank > 1 {
|
|
write!(f, " [{}]", White.paint(&self.rank))?;
|
|
}
|
|
|
|
if let Some(ref format) = self.format {
|
|
write!(f, " {}", Yellow.paint(format))?;
|
|
}
|
|
|
|
if let Some(name) = self.name {
|
|
write!(f, " {}{}{}",
|
|
Cyan.paint("("), Purple.paint(name), Cyan.paint(")"))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Route {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
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()
|
|
}
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
impl<'a> From<&'a StaticRouteInfo> for Route {
|
|
fn from(info: &'a StaticRouteInfo) -> Route {
|
|
// This should never panic since `info.path` is statically checked.
|
|
let mut route = Route::new(info.method, info.path, info.handler);
|
|
route.format = info.format.clone();
|
|
route.name = Some(info.name);
|
|
if let Some(rank) = info.rank {
|
|
route.rank = rank;
|
|
}
|
|
|
|
route
|
|
}
|
|
}
|