use std::{collections::HashMap, convert::Infallible};
use faststr::FastStr;
use http::status::StatusCode;
use metainfo::METAINFO;
use motore::{ServiceExt, layer::Layer, service::Service};
use volo::context::Context;
use super::{
Fallback, Route,
method_router::MethodRouter,
utils::{Matcher, NEST_CATCH_PARAM, RouteId, StripPrefixLayer},
};
use crate::{
body::Body,
context::ServerContext,
request::Request,
response::Response,
server::{IntoResponse, handler::Handler},
};
#[must_use]
pub struct Router<B = Body, E = Infallible> {
matcher: Matcher,
routes: HashMap<RouteId, (Endpoint<B, E>, FastStr)>,
fallback: Fallback<B, E>,
is_default_fallback: bool,
}
impl<B, E> Default for Router<B, E>
where
B: Send + 'static,
E: 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<B, E> Router<B, E>
where
B: Send + 'static,
E: 'static,
{
pub fn new() -> Self
where
E: 'static,
{
Self {
matcher: Default::default(),
routes: Default::default(),
fallback: Fallback::from_status_code(StatusCode::NOT_FOUND),
is_default_fallback: true,
}
}
pub fn route<S>(mut self, uri: S, method_router: MethodRouter<B, E>) -> Self
where
S: AsRef<str>,
{
let route_id = self
.matcher
.insert(uri.as_ref())
.expect("Insert routing rule failed");
self.routes.insert(
route_id,
(
Endpoint::MethodRouter(method_router),
FastStr::new(uri.as_ref()),
),
);
self
}
pub fn nest<U>(self, uri: U, router: Router<B, E>) -> Self
where
U: AsRef<str>,
{
self.nest_route(uri.as_ref().to_owned(), Route::new(router))
}
pub fn nest_service<U, S>(self, uri: U, service: S) -> Self
where
U: AsRef<str>,
S: Service<ServerContext, Request<B>, Error = E> + Send + Sync + 'static,
S::Response: IntoResponse,
{
self.nest_route(
uri.as_ref().to_owned(),
Route::new(service.map_response(IntoResponse::into_response)),
)
}
fn nest_route(mut self, prefix: String, route: Route<B, E>) -> Self {
let uri = if prefix.ends_with('/') {
format!("{prefix}{NEST_CATCH_PARAM}")
} else {
format!("{prefix}/{NEST_CATCH_PARAM}")
};
let route_id = self
.matcher
.insert(prefix.clone())
.expect("Insert routing rule failed");
let pattern = FastStr::new(prefix.trim_end_matches('/'));
if !prefix.ends_with('/') {
let prefix_with_slash = prefix + "/";
self.matcher
.insert_with_id(prefix_with_slash, route_id)
.expect("Insert routing rule failed");
}
self.matcher
.insert_with_id(uri, route_id)
.expect("Insert routing rule failed");
self.routes.insert(
route_id,
(
Endpoint::Service(Route::new(StripPrefixLayer.layer(route))),
pattern,
),
);
self
}
pub fn fallback<H, T>(mut self, handler: H) -> Self
where
for<'a> H: Handler<T, B, E> + Clone + Send + Sync + 'a,
T: 'static,
E: 'static,
{
self.fallback = Fallback::from_handler(handler);
self
}
pub fn fallback_service<S>(mut self, service: S) -> Self
where
for<'a> S: Service<ServerContext, Request<B>, Error = E> + Send + Sync + 'a,
S::Response: IntoResponse,
{
self.fallback = Fallback::from_service(service);
self
}
pub fn merge(mut self, other: Self) -> Self {
let Router {
mut matcher,
mut routes,
fallback,
is_default_fallback,
} = other;
for (path, route_id) in matcher.drain() {
self.matcher
.insert_with_id(path, route_id)
.expect("Insert routing rule failed during merging router");
}
for (route_id, route) in routes.drain() {
if self.routes.insert(route_id, route).is_some() {
unreachable!()
}
}
match (self.is_default_fallback, is_default_fallback) {
(_, true) => {}
(true, false) => {
self.fallback = fallback;
self.is_default_fallback = false;
}
(false, false) => {
panic!("Merge `Router` failed because both `Router` have customized `fallback`")
}
}
self
}
pub fn layer<L, B2, E2>(self, l: L) -> Router<B2, E2>
where
L: Layer<Route<B, E>> + Clone + Send + Sync + 'static,
L::Service: Service<ServerContext, Request<B2>, Error = E2> + Send + Sync + 'static,
<L::Service as Service<ServerContext, Request<B2>>>::Response: IntoResponse,
B2: 'static,
{
let routes = self
.routes
.into_iter()
.map(|(id, (route, pattern))| {
let route = route.layer(l.clone());
(id, (route, pattern))
})
.collect();
let fallback = self.fallback.layer(l.clone());
Router {
matcher: self.matcher,
routes,
fallback,
is_default_fallback: self.is_default_fallback,
}
}
}
impl<B, E> Service<ServerContext, Request<B>> for Router<B, E>
where
B: Send + 'static,
E: 'static,
{
type Response = Response;
type Error = E;
async fn call(
&self,
cx: &mut ServerContext,
req: Request<B>,
) -> Result<Self::Response, Self::Error> {
if let Ok(matched) = self.matcher.at(req.uri().clone().path()) {
if let Some((route, pattern)) = self.routes.get(matched.value) {
if !matched.params.is_empty() {
cx.params_mut().extend(matched.params);
}
MatchedPath::record(cx, pattern);
return route.call(cx, req).await;
}
}
cx.extensions_mut().remove::<MatchedPath>();
let _ = METAINFO.try_with(|mi| mi.borrow_mut().remove_faststr::<MatchedPath>());
self.fallback.call(cx, req).await
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MatchedPath(FastStr);
impl MatchedPath {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> FastStr {
self.0
}
fn record(cx: &mut ServerContext, pattern: &FastStr) {
let extensions = cx.extensions_mut();
let matched = match extensions.get_mut::<MatchedPath>() {
Some(outer) => {
outer.0 = FastStr::from_string(outer.0.to_string() + pattern);
outer.0.clone()
}
None => {
extensions.insert(MatchedPath(pattern.clone()));
pattern.clone()
}
};
let _ = METAINFO.try_with(|mi| mi.borrow_mut().insert_faststr::<MatchedPath>(matched));
}
}
impl std::ops::Deref for MatchedPath {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for MatchedPath {
fn as_ref(&self) -> &str {
&self.0
}
}
#[allow(clippy::large_enum_variant)]
enum Endpoint<B = Body, E = Infallible> {
MethodRouter(MethodRouter<B, E>),
Service(Route<B, E>),
}
impl<B, E> Service<ServerContext, Request<B>> for Endpoint<B, E>
where
B: Send + 'static,
E: 'static,
{
type Response = Response;
type Error = E;
async fn call(
&self,
cx: &mut ServerContext,
req: Request<B>,
) -> Result<Self::Response, Self::Error> {
match self {
Self::MethodRouter(mr) => mr.call(cx, req).await,
Self::Service(service) => service.call(cx, req).await,
}
}
}
impl<B, E> Default for Endpoint<B, E>
where
B: Send + 'static,
E: 'static,
{
fn default() -> Self {
Self::MethodRouter(Default::default())
}
}
impl<B, E> Endpoint<B, E>
where
B: Send + 'static,
E: 'static,
{
fn layer<L, B2, E2>(self, l: L) -> Endpoint<B2, E2>
where
L: Layer<Route<B, E>> + Clone + Send + Sync + 'static,
L::Service: Service<ServerContext, Request<B2>, Error = E2> + Send + Sync,
<L::Service as Service<ServerContext, Request<B2>>>::Response: IntoResponse,
B2: 'static,
{
match self {
Self::MethodRouter(mr) => Endpoint::MethodRouter(mr.layer(l)),
Self::Service(s) => Endpoint::Service(Route::new(
l.layer(s).map_response(IntoResponse::into_response),
)),
}
}
}
#[cfg(test)]
mod router_tests {
use faststr::FastStr;
use http::{method::Method, status::StatusCode, uri::Uri};
use motore::service::Service;
use volo::context::Context;
use super::{MatchedPath, Router};
use crate::{
body::{Body, BodyConversion},
request::Request,
server::{
Server,
param::PathParamsVec,
route::method_router::any,
test_helpers::{TestServer, empty_cx},
},
};
async fn always_ok() {}
async fn teapot() -> StatusCode {
StatusCode::IM_A_TEAPOT
}
#[tokio::test]
async fn url_match() {
async fn is_ok(server: &TestServer<Router<Option<Body>>, Option<Body>>, uri: &str) -> bool {
server.call_route(Method::GET, uri, None).await.status() == StatusCode::OK
}
let router: Router<Option<Body>> = Router::new()
.route("/", any(always_ok))
.route("/catch/{id}", any(always_ok))
.route("/catch/{id}/another", any(always_ok))
.route("/catch/{id}/another/{uid}", any(always_ok))
.route("/catch/{id}/another/{uid}/again", any(always_ok))
.route("/catch/{id}/another/{uid}/again/{tid}", any(always_ok))
.route("/catch_all/{*all}", any(always_ok));
let server = Server::new(router).into_test_server();
assert!(is_ok(&server, "/").await);
assert!(is_ok(&server, "/catch/114").await);
assert!(is_ok(&server, "/catch/514").await);
assert!(is_ok(&server, "/catch/ll45l4").await);
assert!(is_ok(&server, "/catch/ll45l4/another").await);
assert!(is_ok(&server, "/catch/ll45l4/another/1919").await);
assert!(is_ok(&server, "/catch/ll45l4/another/1919/again").await);
assert!(is_ok(&server, "/catch/ll45l4/another/1919/again/810").await);
assert!(is_ok(&server, "/catch_all/114").await);
assert!(is_ok(&server, "/catch_all/114/514/1919/810").await);
assert!(!is_ok(&server, "/catch").await);
assert!(!is_ok(&server, "/catch/114/").await);
assert!(!is_ok(&server, "/catch/114/another/514/").await);
assert!(!is_ok(&server, "/catch/11/another/45/again/14/").await);
assert!(!is_ok(&server, "/catch_all").await);
assert!(!is_ok(&server, "/catch_all/").await);
}
#[tokio::test]
async fn matched_path() {
async fn matched_path_of(router: &Router<Option<Body>>, uri: &str) -> Option<String> {
let mut cx = empty_cx();
let req = Request::builder()
.method(Method::GET)
.uri(uri)
.body(None)
.unwrap();
let _ = router.call(&mut cx, req).await;
cx.extensions()
.get::<MatchedPath>()
.map(|path| path.as_str().to_owned())
}
let router: Router<Option<Body>> = Router::new()
.route("/", any(always_ok))
.route("/users/{id}", any(always_ok))
.route("/files/{*path}", any(always_ok))
.nest(
"/api/{version}",
Router::new().route("/items/{id}", any(always_ok)),
)
.nest("/legacy/", Router::new().route("/", any(always_ok)))
.nest(
"/v/{ver}",
Router::new().nest("/deep", Router::new().route("/leaf/{id}", any(always_ok))),
)
.merge(Router::new().route("/merged/{id}", any(always_ok)));
assert_eq!(matched_path_of(&router, "/").await.as_deref(), Some("/"));
assert_eq!(
matched_path_of(&router, "/users/42").await.as_deref(),
Some("/users/{id}")
);
assert_eq!(
matched_path_of(&router, "/users/43").await.as_deref(),
Some("/users/{id}")
);
assert_eq!(
matched_path_of(&router, "/files/a/b/c").await.as_deref(),
Some("/files/{*path}")
);
assert_eq!(
matched_path_of(&router, "/api/v2/items/7").await.as_deref(),
Some("/api/{version}/items/{id}")
);
assert_eq!(
matched_path_of(&router, "/legacy/").await.as_deref(),
Some("/legacy/")
);
assert_eq!(
matched_path_of(&router, "/v/3/deep/leaf/9")
.await
.as_deref(),
Some("/v/{ver}/deep/leaf/{id}")
);
assert_eq!(
matched_path_of(&router, "/merged/1").await.as_deref(),
Some("/merged/{id}")
);
assert_eq!(matched_path_of(&router, "/nope").await, None);
assert_eq!(matched_path_of(&router, "/api/v2/nope").await, None);
}
#[tokio::test]
async fn matched_path_is_mirrored_into_metainfo() {
use std::cell::RefCell;
use metainfo::{METAINFO, MetaInfo};
async fn both(
router: &Router<Option<Body>>,
uri: &str,
) -> (Option<String>, Option<String>) {
METAINFO
.scope(RefCell::new(MetaInfo::default()), async {
let mut cx = empty_cx();
let req = Request::builder()
.method(Method::GET)
.uri(uri)
.body(None)
.unwrap();
let _ = router.call(&mut cx, req).await;
let from_cx = cx
.extensions()
.get::<MatchedPath>()
.map(|p| p.as_str().to_owned());
let from_mi = METAINFO.with(|mi| {
mi.borrow()
.get_faststr::<MatchedPath>()
.map(|p| p.as_str().to_owned())
});
(from_cx, from_mi)
})
.await
}
let router: Router<Option<Body>> = Router::new().route("/users/{id}", any(always_ok)).nest(
"/api/{version}",
Router::new().route("/items/{id}", any(always_ok)),
);
let (cx, mi) = both(&router, "/users/42").await;
assert_eq!(cx.as_deref(), Some("/users/{id}"));
assert_eq!(mi, cx);
let (cx, mi) = both(&router, "/api/v2/items/7").await;
assert_eq!(cx.as_deref(), Some("/api/{version}/items/{id}"));
assert_eq!(mi, cx);
assert_eq!(both(&router, "/nope").await, (None, None));
assert_eq!(both(&router, "/api/v2/nope").await, (None, None));
}
#[tokio::test]
async fn matched_path_is_visible_mid_request() {
use std::cell::RefCell;
use metainfo::{METAINFO, MetaInfo};
use crate::{
context::ServerContext,
response::Response,
server::{
IntoResponse,
middleware::{Next, from_fn},
},
};
fn current() -> Option<String> {
METAINFO.with(|mi| {
mi.borrow()
.get_faststr::<MatchedPath>()
.map(|p| p.as_str().to_owned())
})
}
async fn echo_matched_path() -> String {
current().unwrap_or_default()
}
struct Seen {
start: Option<String>,
end: Option<String>,
}
async fn probe(
cx: &mut ServerContext,
req: Request<Option<Body>>,
next: Next<Option<Body>>,
) -> Response {
let start = current();
let resp = next.run(cx, req).await.into_response();
cx.extensions_mut().insert(Seen {
start,
end: current(),
});
resp
}
async fn run(router: &Router<Option<Body>>, uri: &str) -> (String, Seen) {
METAINFO
.scope(RefCell::new(MetaInfo::default()), async {
let mut cx = empty_cx();
let req = Request::builder()
.method(Method::GET)
.uri(uri)
.body(None)
.unwrap();
let resp = router.call(&mut cx, req).await.unwrap();
let body = resp.into_body().into_string().await.unwrap();
let seen = cx.extensions_mut().remove::<Seen>().unwrap();
(body, seen)
})
.await
}
let router: Router<Option<Body>> = Router::new()
.route("/users/{id}", any(echo_matched_path))
.nest(
"/api/{version}",
Router::new().route("/items/{id}", any(echo_matched_path)),
)
.layer(from_fn(probe));
let (body, seen) = run(&router, "/users/42").await;
assert_eq!(body, "/users/{id}");
assert_eq!(seen.start.as_deref(), Some("/users/{id}"));
assert_eq!(seen.end.as_deref(), Some("/users/{id}"));
let (body, seen) = run(&router, "/api/v2/items/7").await;
assert_eq!(body, "/api/{version}/items/{id}");
assert_eq!(seen.start.as_deref(), Some("/api/{version}"));
assert_eq!(seen.end.as_deref(), Some("/api/{version}/items/{id}"));
}
#[tokio::test]
async fn router_fallback() {
async fn is_teapot(
server: &TestServer<Router<Option<Body>>, Option<Body>>,
uri: &str,
) -> bool {
server.call_route(Method::GET, uri, None).await.status() == StatusCode::IM_A_TEAPOT
}
let router: Router<Option<Body>> = Router::new()
.route("/", any(always_ok))
.route("/catch/{id}", any(always_ok))
.route("/catch_all/{*all}", any(always_ok))
.fallback(teapot);
let server = Server::new(router).into_test_server();
assert!(is_teapot(&server, "//").await);
assert!(is_teapot(&server, "/catch/").await);
assert!(is_teapot(&server, "/catch_all/").await);
assert!(!is_teapot(&server, "/catch/114").await);
assert!(!is_teapot(&server, "/catch_all/514").await);
assert!(!is_teapot(&server, "/catch_all/114/514/1919/810").await);
}
#[tokio::test]
async fn nest_router() {
async fn uri_and_params(uri: Uri, params: PathParamsVec) -> String {
let mut v = vec![FastStr::from_string(uri.to_string())];
v.extend(params.into_iter().map(|(_, v)| v));
v.join("\n")
}
async fn get_res(
server: &TestServer<Router<Option<Body>>, Option<Body>>,
uri: &str,
) -> String {
server
.call_route(Method::GET, uri, None)
.await
.into_string()
.await
.unwrap()
}
let router: Router<Option<Body>> = Router::new()
.nest(
"/test-1",
Router::new()
.route("/", any(uri_and_params))
.route("/id/{id}", any(uri_and_params))
.route("/catch/{*content}", any(uri_and_params)),
)
.nest(
"/test-2/",
Router::new()
.route("/", any(uri_and_params))
.route("/id/{id}", any(uri_and_params))
.route("/catch/{*content}", any(uri_and_params)),
)
.nest(
"/test-3/{catch}",
Router::new()
.route("/", any(uri_and_params))
.route("/id/{id}", any(uri_and_params))
.route("/catch/{*content}", any(uri_and_params)),
)
.nest(
"/test-4/{catch}/",
Router::new()
.route("/", any(uri_and_params))
.route("/id/{id}", any(uri_and_params))
.route("/catch/{*content}", any(uri_and_params)),
);
let server = Server::new(router).into_test_server();
assert_eq!(get_res(&server, "/test-1").await, "/");
assert_eq!(get_res(&server, "/test-1/").await, "/");
assert_eq!(get_res(&server, "/test-1/id/114").await, "/id/114\n114");
assert_eq!(
get_res(&server, "/test-1/catch/114/514/1919/810").await,
"/catch/114/514/1919/810\n114/514/1919/810",
);
assert!(get_res(&server, "/test-2").await.is_empty());
assert_eq!(get_res(&server, "/test-2/").await, "/");
assert_eq!(get_res(&server, "/test-2/id/114").await, "/id/114\n114");
assert_eq!(
get_res(&server, "/test-2/catch/114/514/1919/810").await,
"/catch/114/514/1919/810\n114/514/1919/810",
);
assert_eq!(get_res(&server, "/test-3/114").await, "/\n114");
assert_eq!(get_res(&server, "/test-3/114/").await, "/\n114");
assert_eq!(
get_res(&server, "/test-3/114/id/514").await,
"/id/514\n114\n514",
);
assert_eq!(
get_res(&server, "/test-3/114/catch/514/1919/810").await,
"/catch/514/1919/810\n114\n514/1919/810",
);
assert!(get_res(&server, "/test-4/114").await.is_empty());
assert_eq!(get_res(&server, "/test-4/114/").await, "/\n114");
assert_eq!(
get_res(&server, "/test-4/114/id/514").await,
"/id/514\n114\n514",
);
assert_eq!(
get_res(&server, "/test-4/114/catch/514/1919/810").await,
"/catch/514/1919/810\n114\n514/1919/810",
);
}
#[tokio::test]
async fn deep_nest_router() {
async fn uri_and_params(uri: Uri, params: PathParamsVec) -> String {
let mut v = vec![FastStr::from_string(uri.to_string())];
v.extend(params.into_iter().map(|(_, v)| v));
v.join("\n")
}
async fn get_res(
server: &TestServer<Router<Option<Body>>, Option<Body>>,
uri: &str,
) -> String {
server
.call_route(Method::GET, uri, None)
.await
.into_string()
.await
.unwrap()
}
let router: Router<Option<Body>> = Router::new().nest(
"/test-1/{catch1}",
Router::new().nest(
"/test-2/{catch2}/",
Router::new().nest(
"/test-3",
Router::new()
.route("/", any(uri_and_params))
.route("/id/{id}", any(uri_and_params))
.route("/catch/{*content}", any(uri_and_params)),
),
),
);
let server = Server::new(router).into_test_server();
assert_eq!(
get_res(&server, "/test-1/114/test-2/514/test-3/").await,
"/\n114\n514",
);
assert_eq!(
get_res(&server, "/test-1/114/test-2/514/test-3/id/1919").await,
"/id/1919\n114\n514\n1919",
);
assert_eq!(
get_res(&server, "/test-1/114/test-2/514/test-3/catch/1919/810").await,
"/catch/1919/810\n114\n514\n1919/810",
);
}
#[tokio::test]
async fn nest_router_with_query() {
async fn get_query(uri: Uri) -> Result<String, StatusCode> {
if let Some(query) = uri.query() {
Ok(query.to_owned())
} else {
Err(StatusCode::BAD_REQUEST)
}
}
async fn get_res(
server: &TestServer<Router<Option<Body>>, Option<Body>>,
uri: &str,
) -> Result<String, StatusCode> {
let resp = server.call_route(Method::GET, uri, None).await;
if resp.status().is_success() {
Ok(resp
.into_string()
.await
.expect("response is not a valid string"))
} else {
Err(resp.status())
}
}
let router: Router<Option<Body>> =
Router::new().nest("/nest", Router::new().route("/query", any(get_query)));
let server = Server::new(router).into_test_server();
assert_eq!(
get_res(&server, "/nest/query?foo=bar").await.unwrap(),
"foo=bar",
);
assert_eq!(get_res(&server, "/nest/query?foo").await.unwrap(), "foo");
assert_eq!(get_res(&server, "/nest/query?").await.unwrap(), "");
assert!(get_res(&server, "/nest/query").await.is_err());
}
#[tokio::test]
async fn deep_nest_router_with_query() {
async fn get_query(uri: Uri) -> Result<String, StatusCode> {
if let Some(query) = uri.query() {
Ok(query.to_owned())
} else {
Err(StatusCode::BAD_REQUEST)
}
}
async fn get_res(
server: &TestServer<Router<Option<Body>>, Option<Body>>,
uri: &str,
) -> Result<String, StatusCode> {
let resp = server.call_route(Method::GET, uri, None).await;
if resp.status().is_success() {
Ok(resp
.into_string()
.await
.expect("response is not a valid string"))
} else {
Err(resp.status())
}
}
let router: Router<Option<Body>> = Router::new().nest(
"/nest-1",
Router::new().nest(
"/nest-2",
Router::new().nest("/nest-3", Router::new().route("/query", any(get_query))),
),
);
let server = Server::new(router).into_test_server();
assert_eq!(
get_res(&server, "/nest-1/nest-2/nest-3/query?foo=bar")
.await
.unwrap(),
"foo=bar",
);
assert_eq!(
get_res(&server, "/nest-1/nest-2/nest-3/query?foo")
.await
.unwrap(),
"foo",
);
assert_eq!(
get_res(&server, "/nest-1/nest-2/nest-3/query?")
.await
.unwrap(),
"",
);
assert!(
get_res(&server, "/nest-1/nest-2/nest-3/query")
.await
.is_err()
);
}
}