use std::{fmt, mem, rc::Rc};
use crate::error::Failure;
use crate::http::Method;
use crate::service::{Ctx, Service, ServiceFactory};
use super::error::{WebError, WebResponseError};
use super::guard::{self, AllGuard, Guard};
use super::handler::{Handler, HandlerFn, HandlerWrapper};
use super::{AppState, FromRequest, HttpResponse, WebRequest, WebResponse};
pub struct Route<St: AppState, In = ()> {
handler: Rc<dyn HandlerFn<St, In>>,
methods: Vec<Method>,
guards: Rc<AllGuard>,
}
impl<St: AppState, In: 'static> Route<St, In> {
pub fn new() -> Route<St, In> {
Route {
handler: Rc::new(HandlerWrapper::<St, In, _, ()>::new(async || {
HttpResponse::NotFound()
})),
methods: Vec::new(),
guards: Rc::default(),
}
}
pub(super) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
for m in &self.methods {
Rc::get_mut(&mut self.guards)
.unwrap()
.add(guard::Method(m.clone()));
}
mem::take(&mut Rc::get_mut(&mut self.guards).unwrap().0)
}
pub(super) fn service(&self) -> RouteService<St, In> {
RouteService {
handler: self.handler.clone(),
guards: self.guards.clone(),
methods: self.methods.clone(),
}
}
}
impl<St: AppState, In: 'static> Default for Route<St, In> {
fn default() -> Self {
Self::new()
}
}
impl<St: AppState, In: 'static> ServiceFactory<St, WebRequest<In>> for Route<St, In> {
type Res = WebResponse;
type Error = WebError<St, St::Error>;
type Service = RouteService<St, In>;
type InitError = Failure;
async fn create(&self, _: &St) -> Result<Self::Service, Self::InitError> {
Ok(self.service())
}
}
impl<St: AppState, In: 'static> Route<St, In> {
#[must_use]
pub fn method(mut self, method: Method) -> Self {
self.methods.push(method);
self
}
#[must_use]
pub fn guard<F: Guard + 'static>(mut self, f: F) -> Self {
Rc::get_mut(&mut self.guards).unwrap().add(f);
self
}
#[must_use]
pub fn to<H, Args>(mut self, handler: H) -> Self
where
H: Handler<St, Args> + 'static,
Args: FromRequest<St> + 'static,
Args::Error: WebResponseError<St, St::Error>,
{
self.handler = Rc::new(HandlerWrapper::new(handler));
self
}
}
impl<St: AppState, In> fmt::Debug for Route<St, In> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Route")
.field("handler", &self.handler)
.field("methods", &self.methods)
.field("guards", &self.guards)
.finish()
}
}
pub struct RouteService<St: AppState, In> {
handler: Rc<dyn HandlerFn<St, In>>,
methods: Vec<Method>,
guards: Rc<AllGuard>,
}
impl<St: AppState, In> RouteService<St, In> {
pub fn check(&self, req: &mut WebRequest<In>) -> bool {
if !self.methods.is_empty() && !self.methods.contains(&req.head().method) {
return false;
}
self.guards.check(req.head())
}
}
impl<St: AppState, In> fmt::Debug for RouteService<St, In> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RouteService")
.field("handler", &self.handler)
.field("methods", &self.methods)
.field("guards", &self.guards)
.finish()
}
}
impl<St: AppState, In> Service<St, WebRequest<In>> for RouteService<St, In> {
type Res = WebResponse;
type Error = WebError<St, St::Error>;
async fn call(
&self,
req: WebRequest<In>,
ctx: Ctx<'_, Self, St>,
) -> Result<Self::Res, Self::Error> {
Ok(self.handler.call(ctx.st(), req).await)
}
}
pub trait IntoRoutes<St: AppState, In> {
fn routes(self) -> Vec<Route<St, In>>;
}
impl<St: AppState, In> IntoRoutes<St, In> for Route<St, In> {
fn routes(self) -> Vec<Route<St, In>> {
vec![self]
}
}
impl<St: AppState, In> IntoRoutes<St, In> for Vec<Route<St, In>> {
fn routes(self) -> Vec<Route<St, In>> {
self
}
}
macro_rules! tuple_routes(
{$(#[$meta:meta])* $(($n:tt, $T:ident)),+} => {
$(#[$meta])*
#[allow(unused_parens)]
impl<St: AppState, U, $($T,)+> IntoRoutes<St, U> for ($($T,)+)
where
$($T: Into<Route<St, U>> + 'static,)+ {
fn routes(self) -> Vec<Route<St, U>> {
vec![$(self.$n.into(),)+]
}
}
}
);
impl<St: AppState, In, T, const N: usize> IntoRoutes<St, In> for [T; N]
where
T: Into<Route<St, In>>,
{
fn routes(self) -> Vec<Route<St, In>> {
let mut routes = Vec::with_capacity(N);
for route in self {
routes.push(route.into());
}
routes
}
}
#[allow(clippy::wildcard_imports)]
#[rustfmt::skip]
mod m {
use variadics_please::all_tuples_enumerated;
use super::*;
all_tuples_enumerated!(#[doc(fake_variadic)] tuple_routes, 1, 12, T);
}
#[cfg(test)]
mod tests {
use crate::http::{Method, StatusCode, header};
use crate::time::{Millis, sleep};
use crate::web::test::{TestRequest, call_service, init_service, read_body};
use crate::web::{self, App, HttpResponse, error, guard};
use crate::{ServiceFactory, util::Bytes};
#[derive(serde::Serialize, PartialEq, Debug)]
struct MyObject {
name: String,
}
#[crate::rt_test]
async fn test_route() {
let srv = init_service(
App::new()
.service(web::resource("/test").route(vec![
web::get().to(async || { HttpResponse::Ok() }),
web::put().to(async || {
Err::<HttpResponse, _>(
error::ErrorBadRequest::<_>("err"),
)
}),
web::post().to(async || {
sleep(Millis(100)).await;
HttpResponse::Created()
}),
web::patch()
.guard(guard::fn_guard(|req|
req.headers().contains_key("content-type")
))
.to(async || { HttpResponse::Conflict() }),
web::delete().to(async || {
sleep(Millis(100)).await;
Err::<HttpResponse, _>(error::ErrorBadRequest("err"))
}),
]))
.service(web::resource("/json").route(web::get().to(async || {
sleep(Millis(25)).await;
web::types::Json(MyObject {
name: "test".to_string(),
})
}))),
)
.await;
let req = TestRequest::with_uri("/test")
.method(Method::GET)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/test")
.method(Method::POST)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::CREATED);
let req = TestRequest::with_uri("/test")
.method(Method::PUT)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/test")
.method(Method::PATCH)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let req = TestRequest::with_uri("/test")
.method(Method::PATCH)
.header(header::CONTENT_TYPE, "text/plain")
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::CONFLICT);
let req = TestRequest::with_uri("/test")
.method(Method::DELETE)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/test")
.method(Method::HEAD)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let req = TestRequest::with_uri("/json").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}"));
let route: web::Route<(), ()> = web::get();
let repr = format!("{route:?}");
assert!(repr.contains("Route"), "{}", repr);
assert!(
repr.contains("handler: Handler(\"ntex::web::route::Route<()>::new::{{closure}}\")"),
"{}",
repr
);
assert!(repr.contains("methods: [GET]"), "{}", repr);
assert!(repr.contains("guards: AllGuard()"), "{}", repr);
assert!(route.create(&()).await.is_ok());
let route_service = route.service();
let repr = format!("{route_service:?}");
assert!(repr.contains("RouteService"));
assert!(
repr.contains("handler: Handler(\"ntex::web::route::Route<()>::new::{{closure}}\")")
);
assert!(repr.contains("methods: [GET]"));
assert!(repr.contains("guards: AllGuard()"));
}
}