use std::{mem, rc::Rc}; use actix_http::{body::MessageBody, Method}; use actix_service::{ apply, boxed::{self, BoxService}, fn_service, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use crate::{ guard::{self, Guard}, handler::{handler_service, Handler}, middleware::Compat, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, Error, FromRequest, HttpResponse, Responder, }; /// A request handler with [guards](guard). /// /// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found` /// handler is used. pub struct Route { service: BoxedHttpServiceFactory, guards: Rc>>, } impl Route { /// Create new route which matches any request. #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { service: boxed::factory(fn_service(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::NotFound())) })), guards: Rc::new(Vec::new()), } } /// Registers a route middleware. /// /// `mw` is a middleware component (type), that can modify the requests and responses handled by /// this `Route`. /// /// See [`App::wrap`](crate::App::wrap) for more details. #[doc(alias = "middleware")] #[doc(alias = "use")] // nodejs terminology pub fn wrap(self, mw: M) -> Route where M: Transform< BoxService, ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > + 'static, B: MessageBody + 'static, { Route { service: boxed::factory(apply(Compat::new(mw), self.service)), guards: self.guards, } } pub(crate) fn take_guards(&mut self) -> Vec> { mem::take(Rc::get_mut(&mut self.guards).unwrap()) } } impl ServiceFactory for Route { type Response = ServiceResponse; type Error = Error; type Config = (); type Service = RouteService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { let fut = self.service.new_service(()); let guards = self.guards.clone(); Box::pin(async move { let service = fut.await?; Ok(RouteService { service, guards }) }) } } pub struct RouteService { service: BoxService, guards: Rc>>, } impl RouteService { // TODO(breaking): remove pass by ref mut #[allow(clippy::needless_pass_by_ref_mut)] pub fn check(&self, req: &mut ServiceRequest) -> bool { let guard_ctx = req.guard_ctx(); for guard in self.guards.iter() { if !guard.check(&guard_ctx) { return false; } } true } } impl Service for RouteService { type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { self.service.call(req) } } impl Route { /// Add method guard to the route. /// /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( /// web::get() /// .method(http::Method::CONNECT) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) /// ); /// # } /// ``` pub fn method(mut self, method: Method) -> Self { Rc::get_mut(&mut self.guards) .unwrap() .push(Box::new(guard::Method(method))); self } /// Add guard to the route. /// /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( /// web::route() /// .guard(guard::Get()) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) /// ); /// # } /// ``` pub fn guard(mut self, f: F) -> Self { Rc::get_mut(&mut self.guards).unwrap().push(Box::new(f)); self } /// Set handler function, use request extractors for parameters. /// /// # Examples /// ``` /// use actix_web::{web, http, App}; /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// /// /// extract path info using serde /// async fn index(info: web::Path) -> String { /// format!("Welcome {}!", info.username) /// } /// /// let app = App::new().service( /// web::resource("/{username}/index.html") // <- define path parameters /// .route(web::get().to(index)) // <- register handler /// ); /// ``` /// /// It is possible to use multiple extractors for one handler function. /// ``` /// # use std::collections::HashMap; /// # use serde::Deserialize; /// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// /// /// extract path info using serde /// async fn index( /// path: web::Path, /// query: web::Query>, /// body: web::Json /// ) -> String { /// format!("Welcome {}!", path.username) /// } /// /// let app = App::new().service( /// web::resource("/{username}/index.html") // <- define path parameters /// .route(web::get().to(index)) /// ); /// ``` pub fn to(mut self, handler: F) -> Self where F: Handler, Args: FromRequest + 'static, F::Output: Responder + 'static, { self.service = handler_service(handler); self } /// Set raw service to be constructed and called as the request handler. /// /// # Examples /// ``` /// # use std::convert::Infallible; /// # use futures_util::future::LocalBoxFuture; /// # use actix_web::{*, dev::*, http::header}; /// struct HelloWorld; /// /// impl Service for HelloWorld { /// type Response = ServiceResponse; /// type Error = Infallible; /// type Future = LocalBoxFuture<'static, Result>; /// /// dev::always_ready!(); /// /// fn call(&self, req: ServiceRequest) -> Self::Future { /// let (req, _) = req.into_parts(); /// /// let res = HttpResponse::Ok() /// .insert_header(header::ContentType::plaintext()) /// .body("Hello world!"); /// /// Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) /// } /// } /// /// App::new().route( /// "/", /// web::get().service(fn_factory(|| async { Ok(HelloWorld) })), /// ); /// ``` pub fn service(mut self, service_factory: S) -> Self where S: ServiceFactory< ServiceRequest, Response = ServiceResponse, Error = E, InitError = (), Config = (), > + 'static, E: Into + 'static, { self.service = boxed::factory(service_factory.map_err(Into::into)); self } } #[cfg(test)] mod tests { use std::{convert::Infallible, time::Duration}; use actix_rt::time::sleep; use bytes::Bytes; use futures_core::future::LocalBoxFuture; use serde::Serialize; use crate::{ dev::{always_ready, fn_factory, fn_service, Service}, error, http::{header, Method, StatusCode}, middleware::{DefaultHeaders, Logger}, service::{ServiceRequest, ServiceResponse}, test::{call_service, init_service, read_body, TestRequest}, web, App, HttpResponse, }; #[derive(Serialize, PartialEq, Debug)] struct MyObject { name: String, } #[actix_rt::test] async fn test_route() { let srv = init_service( App::new() .service( web::resource("/test") .route(web::get().to(HttpResponse::Ok)) .route(web::put().to(|| async { Err::(error::ErrorBadRequest("err")) })) .route(web::post().to(|| async { sleep(Duration::from_millis(100)).await; Ok::<_, Infallible>(HttpResponse::Created()) })) .route(web::delete().to(|| async { sleep(Duration::from_millis(100)).await; Err::(error::ErrorBadRequest("err")) })), ) .service(web::resource("/json").route(web::get().to(|| async { sleep(Duration::from_millis(25)).await; web::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::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\"}")); } #[actix_rt::test] async fn route_middleware() { let srv = init_service( App::new() .route("/", web::get().to(HttpResponse::Ok).wrap(Logger::default())) .service( web::resource("/test") .route(web::get().to(HttpResponse::Ok)) .route( web::post() .to(HttpResponse::Created) .wrap(DefaultHeaders::new().add(("x-test", "x-posted"))), ) .route( web::delete() .to(HttpResponse::Accepted) // logger changes body type, proving Compat is not needed .wrap(Logger::default()), ), ), ) .await; let req = TestRequest::get().uri("/test").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert!(!res.headers().contains_key("x-test")); let req = TestRequest::post().uri("/test").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::CREATED); assert_eq!(res.headers().get("x-test").unwrap(), "x-posted"); let req = TestRequest::delete().uri("/test").to_request(); let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::ACCEPTED); } #[actix_rt::test] async fn test_service_handler() { struct HelloWorld; impl Service for HelloWorld { type Response = ServiceResponse; type Error = crate::Error; type Future = LocalBoxFuture<'static, Result>; always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let (req, _) = req.into_parts(); let res = HttpResponse::Ok() .insert_header(header::ContentType::plaintext()) .body("Hello world!"); Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) } } let srv = init_service( App::new() .route( "/hello", web::get().service(fn_factory(|| async { Ok(HelloWorld) })), ) .route( "/bye", web::get().service(fn_factory(|| async { Ok::<_, ()>(fn_service(|req: ServiceRequest| async { let (req, _) = req.into_parts(); let res = HttpResponse::Ok() .insert_header(header::ContentType::plaintext()) .body("Goodbye, and thanks for all the fish!"); Ok::<_, Infallible>(ServiceResponse::new(req, res)) })) })), ), ) .await; let req = TestRequest::get().uri("/hello").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"Hello world!")); let req = TestRequest::get().uri("/bye").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"Goodbye, and thanks for all the fish!") ); } }