From 34171fa7f570a8188491445fec86a2d427e48bb0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 21:02:01 -0800 Subject: [PATCH] add scopes --- Cargo.toml | 1 + src/app.rs | 120 ++++++- src/guard.rs | 4 +- src/lib.rs | 2 + src/scope.rs | 872 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/test.rs | 9 +- 6 files changed, 1004 insertions(+), 4 deletions(-) create mode 100644 src/scope.rs diff --git a/Cargo.toml b/Cargo.toml index 03b1794ff..370089e7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ actix-utils = "0.3.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" diff --git a/src/app.rs b/src/app.rs index 34a663ae7..276580110 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::resource::Resource; +use crate::scope::Scope; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; @@ -112,6 +113,52 @@ where self } + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().scope("/{project_id}", |scope| { + /// scope + /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(self, path: &str, f: F) -> AppRouter> + where + F: FnOnce(Scope

) -> Scope

, + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + let default = scope.get_default(); + + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, + services: vec![(rdef, boxed::new_service(scope.into_new_service()))], + default: None, + defaults: vec![default], + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: self.extensions, + state: self.state, + _t: PhantomData, + } + } + /// Configure resource for a specific path. /// /// Resources may have variable path segments. For example, a @@ -243,6 +290,18 @@ where _t: PhantomData, } } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn hostname(self, _val: &str) -> Self { + // self.host = val.to_owned(); + self + } } /// Application router builder - Structure that follows the builder pattern @@ -270,6 +329,42 @@ where InitError = (), >, { + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().scope("/{project_id}", |scope| { + /// scope + /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Scope

) -> Scope

, + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + self.defaults.push(scope.get_default()); + self.services + .push((rdef, boxed::new_service(scope.into_new_service()))); + self + } + /// Configure resource for a specific path. /// /// Resources may have variable path segments. For example, a @@ -466,6 +561,7 @@ where // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { + default: self.default.clone(), services: Rc::new(self.services), }); @@ -480,6 +576,7 @@ where pub struct AppRoutingFactory

{ services: Rc)>>, + default: Option>>, } impl NewService for AppRoutingFactory

{ @@ -491,6 +588,12 @@ impl NewService for AppRoutingFactory

{ type Future = AppRoutingFactoryResponse

; fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = self.default { + Some(default.new_service(&())) + } else { + None + }; + AppRoutingFactoryResponse { fut: self .services @@ -502,6 +605,8 @@ impl NewService for AppRoutingFactory

{ ) }) .collect(), + default: None, + default_fut, } } } @@ -512,6 +617,8 @@ type HttpServiceFut

= Box, Error = ()>>; #[doc(hidden)] pub struct AppRoutingFactoryResponse

{ fut: Vec>, + default: Option>, + default_fut: Option, Error = ()>>>, } enum CreateAppRoutingItem

{ @@ -526,6 +633,13 @@ impl

Future for AppRoutingFactoryResponse

{ fn poll(&mut self) -> Poll { let mut done = true; + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + // poll http services for item in &mut self.fut { let res = match item { @@ -560,8 +674,9 @@ impl

Future for AppRoutingFactoryResponse

{ router }); Ok(Async::Ready(AppRouting { - router: router.finish(), ready: None, + router: router.finish(), + default: self.default.take(), })) } else { Ok(Async::NotReady) @@ -572,6 +687,7 @@ impl

Future for AppRoutingFactoryResponse

{ pub struct AppRouting

{ router: Router>, ready: Option<(ServiceRequest

, ResourceInfo)>, + default: Option>, } impl

Service for AppRouting

{ @@ -591,6 +707,8 @@ impl

Service for AppRouting

{ fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) } else { let req = req.into_request(); Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) diff --git a/src/guard.rs b/src/guard.rs index 10a56921a..c8948b3b2 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -312,7 +312,9 @@ mod tests { #[test] fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).to_request(); + let r = TestRequest::default() + .method(Method::TRACE) + .to_http_request(); assert!(Not(Get()).check(&r,)); assert!(!Not(Trace()).check(&r,)); diff --git a/src/lib.rs b/src/lib.rs index 37fd75912..18a6de067 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ mod request; mod resource; mod responder; mod route; +mod scope; mod service; mod state; pub mod test; @@ -25,6 +26,7 @@ pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; +pub use crate::scope::Scope; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 000000000..5327f953e --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,872 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use actix_http::Response; +use actix_router::{ResourceDef, ResourceInfo, Router}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_service::{ + ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, +}; +use futures::future::{ok, Either, Future, FutureResult}; +use futures::{Async, Poll}; + +use crate::guard::Guard; +use crate::resource::Resource; +use crate::route::Route; +use crate::service::{ServiceRequest, ServiceResponse}; + +type HttpService

= BoxedService, ServiceResponse, ()>; +type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; +type BoxedResponse = Box>; + +/// Resources scope +/// +/// Scope is a set of resources with common root path. +/// Scopes collect multiple paths under a common path prefix. +/// Scope path can contain variable path segments as resources. +/// Scope prefix is always complete path segment, i.e `/app` would +/// be converted to a `/app/` and it would not match `/app` path. +/// +/// You can get variable path segments from `HttpRequest::match_info()`. +/// `Path` extractor also is able to extract scope level variable segments. +/// +/// ```rust +/// use actix_web::{App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().scope("/{project_id}/", |scope| { +/// scope +/// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) +/// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) +/// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) +/// }); +/// } +/// ``` +/// +/// In the above example three routes get registered: +/// * /{project_id}/path1 - reponds to all http method +/// * /{project_id}/path2 - `GET` requests +/// * /{project_id}/path3 - `HEAD` requests +/// +pub struct Scope> { + endpoint: T, + rdef: ResourceDef, + services: Vec<(ResourceDef, HttpNewService

)>, + guards: Rc>>, + default: Rc>>>>, + defaults: Vec>>>>>, + factory_ref: Rc>>>, +} + +impl Scope

{ + /// Create a new scope + pub fn new(path: &str) -> Scope

{ + let fref = Rc::new(RefCell::new(None)); + let rdef = ResourceDef::prefix(&insert_slash(path)); + Scope { + endpoint: ScopeEndpoint::new(fref.clone()), + rdef: rdef.clone(), + guards: Rc::new(Vec::new()), + services: Vec::new(), + default: Rc::new(RefCell::new(None)), + defaults: Vec::new(), + factory_ref: fref, + } + } +} + +impl Scope +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + #[inline] + pub(crate) fn rdef(&self) -> &ResourceDef { + &self.rdef + } + + /// Add guard to a scope. + /// + /// ```rust + /// use actix_web::{web, guard, App, HttpRequest, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope + /// .guard(guard::Header("content-type", "text/plain")) + /// .route("/test1",web::get().to(index)) + /// .route("/test2", web::post().to(|r: HttpRequest| { + /// HttpResponse::MethodNotAllowed() + /// })) + /// }); + /// } + /// ``` + pub fn guard(mut self, guard: G) -> Self { + Rc::get_mut(&mut self.guards).unwrap().push(Box::new(guard)); + self + } + + /// Create nested scope. + /// + /// ```rust + /// use actix_web::{App, HttpRequest}; + /// + /// struct AppState; + /// + /// fn index(req: HttpRequest) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.to(index))) + /// }); + /// } + /// ``` + pub fn nested(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Scope

) -> Scope

, + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + self.defaults.push(scope.get_default()); + self.services + .push((rdef, boxed::new_service(scope.into_new_service()))); + + self + } + + /// Configure route for a specific path. + /// + /// This is a simplified version of the `Scope::resource()` method. + /// This method can not be could multiple times, in that case + /// multiple resources with one route would be registered for same resource path. + /// + /// ```rust + /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope.route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + pub fn route(self, path: &str, route: Route

) -> Self { + self.resource(path, move |r| r.route(route)) + } + + /// configure resource for a specific path. + /// + /// This method is similar to an `App::resource()` method. + /// Resources may have variable path segments. Resource path uses scope + /// path as a path prefix. + /// + /// ```rust + /// use actix_web::*; + /// + /// fn main() { + /// let app = App::new().scope("/api", |scope| { + /// scope.resource("/users/{userid}/{friend}", |r| { + /// r.route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// .route(web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// }) + /// }); + /// } + /// ``` + pub fn resource(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Resource

) -> Resource, + U: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // add resource + let rdef = ResourceDef::new(&insert_slash(path)); + let resource = f(Resource::new()); + self.defaults.push(resource.get_default()); + self.services + .push((rdef, boxed::new_service(resource.into_new_service()))); + self + } + + /// Default resource to be used if no matching route could be found. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

) -> Resource, + U: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( + f(Resource::new()).into_new_service().map_init_err(|_| ()), + ))))); + + self + } + + /// Register a scope middleware + /// + /// This is similar to `App's` middlewares, but + /// middleware is not allowed to change response type (i.e modify response's body). + /// Middleware get invoked on scope level. + pub fn middleware( + self, + mw: F, + ) -> Scope< + P, + impl NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, + > + where + M: NewTransform< + T::Service, + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, + F: IntoNewTransform, + { + let endpoint = ApplyNewService::new(mw, self.endpoint); + Scope { + endpoint, + rdef: self.rdef, + guards: self.guards, + services: self.services, + default: self.default, + defaults: self.defaults, + factory_ref: self.factory_ref, + } + } + + pub(crate) fn get_default(&self) -> Rc>>>> { + self.default.clone() + } +} + +fn insert_slash(path: &str) -> String { + let mut path = path.to_owned(); + if !path.is_empty() && !path.starts_with('/') { + path.insert(0, '/'); + }; + path +} + +impl IntoNewService for Scope +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + fn into_new_service(self) -> T { + // update resource default service + if let Some(ref d) = *self.default.as_ref().borrow() { + for default in &self.defaults { + if default.borrow_mut().is_none() { + *default.borrow_mut() = Some(d.clone()); + } + } + } + + *self.factory_ref.borrow_mut() = Some(ScopeFactory { + default: self.default.clone(), + services: Rc::new(self.services), + }); + + self.endpoint + } +} + +pub struct ScopeFactory

{ + services: Rc)>>, + default: Rc>>>>, +} + +impl NewService for ScopeFactory

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ScopeService

; + type Future = ScopeFactoryResponse

; + + fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = *self.default.borrow() { + Some(default.new_service(&())) + } else { + None + }; + + ScopeFactoryResponse { + fut: self + .services + .iter() + .map(|(path, service)| { + CreateScopeServiceItem::Future( + Some(path.clone()), + service.new_service(&()), + ) + }) + .collect(), + default: None, + default_fut, + } + } +} + +/// Create app service +#[doc(hidden)] +pub struct ScopeFactoryResponse

{ + fut: Vec>, + default: Option>, + default_fut: Option, Error = ()>>>, +} + +type HttpServiceFut

= Box, Error = ()>>; + +enum CreateScopeServiceItem

{ + Future(Option, HttpServiceFut

), + Service(ResourceDef, HttpService

), +} + +impl

Future for ScopeFactoryResponse

{ + type Item = ScopeService

; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateScopeServiceItem::Future(ref mut path, ref mut fut) => { + match fut.poll()? { + Async::Ready(service) => Some((path.take().unwrap(), service)), + Async::NotReady => { + done = false; + None + } + } + } + CreateScopeServiceItem::Service(_, _) => continue, + }; + + if let Some((path, service)) = res { + *item = CreateScopeServiceItem::Service(path, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateScopeServiceItem::Service(path, service) => { + router.rdef(path, service) + } + CreateScopeServiceItem::Future(_, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(ScopeService { + router: router.finish(), + default: self.default.take(), + _ready: None, + })) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct ScopeService

{ + router: Router>, + default: Option>, + _ready: Option<(ServiceRequest

, ResourceInfo)>, +} + +impl

Service for ScopeService

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type Future = Either>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) + } else { + let req = req.into_request(); + Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + } + } +} + +#[doc(hidden)] +pub struct ScopeEndpoint

{ + factory: Rc>>>, +} + +impl

ScopeEndpoint

{ + fn new(factory: Rc>>>) -> Self { + ScopeEndpoint { factory } + } +} + +impl NewService for ScopeEndpoint

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ScopeService

; + type Future = ScopeFactoryResponse

; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) + } +} + +#[cfg(test)] +mod tests { + use actix_http::body::{Body, ResponseBody}; + use actix_http::http::{Method, StatusCode}; + use actix_service::{IntoNewService, NewService, Service}; + use bytes::Bytes; + + use crate::test::TestRequest; + use crate::{web, App, HttpRequest, HttpResponse}; + + #[test] + fn test_scope() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_scope_root() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_scope_root2() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app/", |scope| { + scope.resource("", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_scope_root3() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app/", |scope| { + scope.resource("/", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_scope_route() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("app", |scope| { + scope.resource("/path1", |r| { + r.route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::DELETE) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_scope_route_without_leading_slash() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("app", |scope| { + scope.resource("path1", |r| { + r.route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::DELETE) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + // #[test] + // fn test_scope_guard() { + // let mut rt = actix_rt::Runtime::new().unwrap(); + // let app = App::new() + // .scope("/app", |scope| { + // scope + // .guard(guard::Get()) + // .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + // }) + // .into_new_service(); + // let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + // let req = TestRequest::with_uri("/app/path1") + // .method(Method::POST) + // .to_request(); + // let resp = rt.block_on(srv.call(req)).unwrap(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/app/path1") + // .method(Method::GET) + // .to_request(); + // let resp = rt.block_on(srv.call(req)).unwrap(); + // assert_eq!(resp.status(), StatusCode::OK); + // } + + #[test] + fn test_scope_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/ab-{project}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Ok() + .body(format!("project: {}", &r.match_info()["project"])) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/ab-project1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: project1")); + } + _ => panic!(), + } + + let req = TestRequest::with_uri("/aa-project1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_nested_scope() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_nested_scope_no_slash() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("t1", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_nested_scope_root() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/t1/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + // #[test] + // fn test_nested_scope_filter() { + // let app = App::new() + // .scope("/app", |scope| { + // scope.nested("/t1", |scope| { + // scope + // .filter(pred::Get()) + // .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + // }) + // }) + // .finish(); + + // let req = TestRequest::with_uri("/app/t1/path1") + // .method(Method::POST) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/app/t1/path1") + // .method(Method::GET) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + // } + + #[test] + fn test_nested_scope_with_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/{project_id}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {}", + &r.match_info()["project_id"] + )) + }) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/project_1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: project_1")); + } + _ => panic!(), + } + } + + #[test] + fn test_nested2_scope_with_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/{project}", |scope| { + scope.nested("/{id}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {} - {}", + &r.match_info()["project"], + &r.match_info()["id"], + )) + }) + }) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/test/1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); + } + _ => panic!(), + } + + let req = TestRequest::with_uri("/app/test/1/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_default_resource() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::BadRequest())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_default_resource_propagation() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app1", |scope| { + scope.default_resource(|r| r.to(|| HttpResponse::BadRequest())) + }) + .scope("/app2", |scope| scope) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let req = TestRequest::with_uri("/app1/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/app2/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } +} diff --git a/src/test.rs b/src/test.rs index 46fd45d5b..684817ec1 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{Extensions, PayloadStream}; +use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; use bytes::Bytes; @@ -135,8 +135,13 @@ impl TestRequest { ) } + /// Complete request creation and generate `Request` instance + pub fn to_request(mut self) -> Request { + self.req.finish() + } + /// Complete request creation and generate `HttpRequest` instance - pub fn to_request(mut self) -> HttpRequest { + pub fn to_http_request(mut self) -> HttpRequest { let req = self.req.finish(); ServiceRequest::new(