diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 000000000..eae5d55a0 --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,601 @@ +use std::cell::UnsafeCell; +use std::marker::PhantomData; +use std::rc::Rc; + +use futures::{Async, Future, Poll}; + +use error::Error; +use handler::{FromRequest, Reply, ReplyItem, Responder, RouteHandler}; +use http::Method; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Finished as MiddlewareFinished, Middleware, + Response as MiddlewareResponse, Started as MiddlewareStarted}; +use resource::ResourceHandler; +use router::Resource; + +type ScopeResources = Rc>>)>>; + +/// Resources scope +/// +/// Scope is a set of resources with common root path. +/// Scopes collect multiple paths under a common path prefix. +/// Scope path can not contain variable path segments as resources. +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::{http, App, HttpRequest, HttpResponse}; +/// +/// fn main() { +/// let app = App::new() +/// .scope("/app", |scope| { +/// scope.resource("/path1", |r| r.f(|_| HttpResponse::Ok())) +/// .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) +/// .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) +/// }); +/// } +/// ``` +/// +/// In the above example three routes get registered: +/// * /app/path1 - reponds to all http method +/// * /app/path2 - `GET` requests +/// * /app/path3 - `HEAD` requests +/// +pub struct Scope { + middlewares: Rc>>>, + default: Rc>>, + resources: ScopeResources, +} + +impl Default for Scope { + fn default() -> Scope { + Scope::new() + } +} + +impl Scope { + pub fn new() -> Scope { + Scope { + resources: Rc::new(Vec::new()), + middlewares: Rc::new(Vec::new()), + default: Rc::new(UnsafeCell::new(ResourceHandler::default_not_found())), + } + } + + /// Configure route for a specific path. + /// + /// This is a simplified version of the `Scope::resource()` method. + /// Handler functions need to accept one request extractor + /// argument. + /// + /// This method could be called multiple times, in that case + /// multiple routes would be registered for same resource path. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{http, App, HttpRequest, HttpResponse, Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .scope("/app", |scope| { + /// scope.route("/test1", http::Method::GET, index) + /// .route("/test2", http::Method::POST, + /// |_: HttpRequest| HttpResponse::MethodNotAllowed()) + /// }); + /// } + /// ``` + pub fn route(mut self, path: &str, method: Method, f: F) -> Scope + where + F: Fn(T) -> R + 'static, + R: Responder + 'static, + T: FromRequest + 'static, + { + // get resource handler + let slf: &Scope = unsafe { &*(&self as *const _) }; + for &(ref pattern, ref resource) in slf.resources.iter() { + if pattern.pattern() == path { + let resource = unsafe { &mut *resource.get() }; + resource.method(method).with(f); + return self; + } + } + + let mut handler = ResourceHandler::default(); + handler.method(method).with(f); + let pattern = Resource::new(handler.get_name(), path); + Rc::get_mut(&mut self.resources) + .expect("Can not use after configuration") + .push((pattern, Rc::new(UnsafeCell::new(handler)))); + + self + } + + /// 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 + /// # extern crate actix_web; + /// use actix_web::*; + /// + /// fn main() { + /// let app = App::new() + /// .scope("/api", |scope| { + /// scope.resource("/users/{userid}/{friend}", |r| { + /// r.get().f(|_| HttpResponse::Ok()); + /// r.head().f(|_| HttpResponse::MethodNotAllowed()); + /// r.route() + /// .filter(pred::Any(pred::Get()).or(pred::Put())) + /// .filter(pred::Header("Content-Type", "text/plain")) + /// .f(|_| HttpResponse::Ok()) + /// }) + /// }); + /// } + /// ``` + pub fn resource(mut self, path: &str, f: F) -> Scope + where + F: FnOnce(&mut ResourceHandler) -> R + 'static, + { + // add resource handler + let mut handler = ResourceHandler::default(); + f(&mut handler); + + let pattern = Resource::new(handler.get_name(), path); + Rc::get_mut(&mut self.resources) + .expect("Can not use after configuration") + .push((pattern, Rc::new(UnsafeCell::new(handler)))); + + self + } + + /// Default resource to be used if no matching route could be found. + pub fn default_resource(self, f: F) -> Scope + where + F: FnOnce(&mut ResourceHandler) -> R + 'static, + { + let default = unsafe { &mut *self.default.as_ref().get() }; + f(default); + self + } + + /// Register a scope middleware + /// + /// This is similar to `App's` middlewares, but + /// middlewares get invoked on scope level. + /// + /// *Note* `Middleware::finish()` is fired right after response get + /// prepared. It does not wait until body get sent to peer. + pub fn middleware>(mut self, mw: M) -> Scope { + Rc::get_mut(&mut self.middlewares) + .expect("Can not use after configuration") + .push(Box::new(mw)); + self + } +} + +impl RouteHandler for Scope { + fn handle(&mut self, mut req: HttpRequest) -> Reply { + let path = unsafe { &*(&req.match_info()["tail"] as *const _) }; + let path = if path == "" { "/" } else { path }; + + for (pattern, resource) in self.resources.iter() { + if pattern.match_with_params(path, req.match_info_mut()) { + let default = unsafe { &mut *self.default.as_ref().get() }; + + if self.middlewares.is_empty() { + let resource = unsafe { &mut *resource.get() }; + return resource.handle(req, Some(default)); + } else { + return Reply::async(Compose::new( + req, + Rc::clone(&self.middlewares), + Rc::clone(&resource), + Some(Rc::clone(&self.default)), + )); + } + } + } + + let default = unsafe { &mut *self.default.as_ref().get() }; + if self.middlewares.is_empty() { + default.handle(req, None) + } else { + Reply::async(Compose::new( + req, + Rc::clone(&self.middlewares), + Rc::clone(&self.default), + None, + )) + } + } +} + +/// Compose resource level middlewares with route handler. +struct Compose { + info: ComposeInfo, + state: ComposeState, +} + +struct ComposeInfo { + count: usize, + req: HttpRequest, + mws: Rc>>>, + default: Option>>>, + resource: Rc>>, +} + +enum ComposeState { + Starting(StartMiddlewares), + Handler(WaitingResponse), + RunMiddlewares(RunMiddlewares), + Finishing(FinishingMiddlewares), + Completed(Response), +} + +impl ComposeState { + fn poll(&mut self, info: &mut ComposeInfo) -> Option> { + match *self { + ComposeState::Starting(ref mut state) => state.poll(info), + ComposeState::Handler(ref mut state) => state.poll(info), + ComposeState::RunMiddlewares(ref mut state) => state.poll(info), + ComposeState::Finishing(ref mut state) => state.poll(info), + ComposeState::Completed(_) => None, + } + } +} + +impl Compose { + fn new( + req: HttpRequest, mws: Rc>>>, + resource: Rc>>, + default: Option>>>, + ) -> Self { + let mut info = ComposeInfo { + count: 0, + req, + mws, + resource, + default, + }; + let state = StartMiddlewares::init(&mut info); + + Compose { state, info } + } +} + +impl Future for Compose { + type Item = HttpResponse; + type Error = Error; + + fn poll(&mut self) -> Poll { + loop { + if let ComposeState::Completed(ref mut resp) = self.state { + let resp = resp.resp.take().unwrap(); + return Ok(Async::Ready(resp)); + } + if let Some(state) = self.state.poll(&mut self.info) { + self.state = state; + } else { + return Ok(Async::NotReady); + } + } + } +} + +/// Middlewares start executor +struct StartMiddlewares { + fut: Option, + _s: PhantomData, +} + +type Fut = Box, Error = Error>>; + +impl StartMiddlewares { + fn init(info: &mut ComposeInfo) -> ComposeState { + let len = info.mws.len(); + loop { + if info.count == len { + let resource = unsafe { &mut *info.resource.get() }; + let reply = if let Some(ref default) = info.default { + let d = unsafe { &mut *default.as_ref().get() }; + resource.handle(info.req.clone(), Some(d)) + } else { + resource.handle(info.req.clone(), None) + }; + return WaitingResponse::init(info, reply); + } else { + match info.mws[info.count].start(&mut info.req) { + Ok(MiddlewareStarted::Done) => info.count += 1, + Ok(MiddlewareStarted::Response(resp)) => { + return RunMiddlewares::init(info, resp) + } + Ok(MiddlewareStarted::Future(mut fut)) => match fut.poll() { + Ok(Async::NotReady) => { + return ComposeState::Starting(StartMiddlewares { + fut: Some(fut), + _s: PhantomData, + }) + } + Ok(Async::Ready(resp)) => { + if let Some(resp) = resp { + return RunMiddlewares::init(info, resp); + } + info.count += 1; + } + Err(err) => return Response::init(err.into()), + }, + Err(err) => return Response::init(err.into()), + } + } + } + } + + fn poll(&mut self, info: &mut ComposeInfo) -> Option> { + let len = info.mws.len(); + 'outer: loop { + match self.fut.as_mut().unwrap().poll() { + Ok(Async::NotReady) => return None, + Ok(Async::Ready(resp)) => { + info.count += 1; + if let Some(resp) = resp { + return Some(RunMiddlewares::init(info, resp)); + } + if info.count == len { + let resource = unsafe { &mut *info.resource.get() }; + let reply = if let Some(ref default) = info.default { + let d = unsafe { &mut *default.as_ref().get() }; + resource.handle(info.req.clone(), Some(d)) + } else { + resource.handle(info.req.clone(), None) + }; + return Some(WaitingResponse::init(info, reply)); + } else { + loop { + match info.mws[info.count].start(&mut info.req) { + Ok(MiddlewareStarted::Done) => info.count += 1, + Ok(MiddlewareStarted::Response(resp)) => { + return Some(RunMiddlewares::init(info, resp)); + } + Ok(MiddlewareStarted::Future(fut)) => { + self.fut = Some(fut); + continue 'outer; + } + Err(err) => return Some(Response::init(err.into())), + } + } + } + } + Err(err) => return Some(Response::init(err.into())), + } + } + } +} + +// waiting for response +struct WaitingResponse { + fut: Box>, + _s: PhantomData, +} + +impl WaitingResponse { + #[inline] + fn init(info: &mut ComposeInfo, reply: Reply) -> ComposeState { + match reply.into() { + ReplyItem::Message(resp) => RunMiddlewares::init(info, resp), + ReplyItem::Future(fut) => ComposeState::Handler(WaitingResponse { + fut, + _s: PhantomData, + }), + } + } + + fn poll(&mut self, info: &mut ComposeInfo) -> Option> { + match self.fut.poll() { + Ok(Async::NotReady) => None, + Ok(Async::Ready(response)) => Some(RunMiddlewares::init(info, response)), + Err(err) => Some(RunMiddlewares::init(info, err.into())), + } + } +} + +/// Middlewares response executor +struct RunMiddlewares { + curr: usize, + fut: Option>>, + _s: PhantomData, +} + +impl RunMiddlewares { + fn init(info: &mut ComposeInfo, mut resp: HttpResponse) -> ComposeState { + let mut curr = 0; + let len = info.mws.len(); + + loop { + resp = match info.mws[curr].response(&mut info.req, resp) { + Err(err) => { + info.count = curr + 1; + return FinishingMiddlewares::init(info, err.into()); + } + Ok(MiddlewareResponse::Done(r)) => { + curr += 1; + if curr == len { + return FinishingMiddlewares::init(info, r); + } else { + r + } + } + Ok(MiddlewareResponse::Future(fut)) => { + return ComposeState::RunMiddlewares(RunMiddlewares { + curr, + fut: Some(fut), + _s: PhantomData, + }) + } + }; + } + } + + fn poll(&mut self, info: &mut ComposeInfo) -> Option> { + let len = info.mws.len(); + + loop { + // poll latest fut + let mut resp = match self.fut.as_mut().unwrap().poll() { + Ok(Async::NotReady) => return None, + Ok(Async::Ready(resp)) => { + self.curr += 1; + resp + } + Err(err) => return Some(FinishingMiddlewares::init(info, err.into())), + }; + + loop { + if self.curr == len { + return Some(FinishingMiddlewares::init(info, resp)); + } else { + match info.mws[self.curr].response(&mut info.req, resp) { + Err(err) => { + return Some(FinishingMiddlewares::init(info, err.into())) + } + Ok(MiddlewareResponse::Done(r)) => { + self.curr += 1; + resp = r + } + Ok(MiddlewareResponse::Future(fut)) => { + self.fut = Some(fut); + break; + } + } + } + } + } + } +} + +/// Middlewares start executor +struct FinishingMiddlewares { + resp: Option, + fut: Option>>, + _s: PhantomData, +} + +impl FinishingMiddlewares { + fn init(info: &mut ComposeInfo, resp: HttpResponse) -> ComposeState { + if info.count == 0 { + Response::init(resp) + } else { + let mut state = FinishingMiddlewares { + resp: Some(resp), + fut: None, + _s: PhantomData, + }; + if let Some(st) = state.poll(info) { + st + } else { + ComposeState::Finishing(state) + } + } + } + + fn poll(&mut self, info: &mut ComposeInfo) -> Option> { + loop { + // poll latest fut + let not_ready = if let Some(ref mut fut) = self.fut { + match fut.poll() { + Ok(Async::NotReady) => true, + Ok(Async::Ready(())) => false, + Err(err) => { + error!("Middleware finish error: {}", err); + false + } + } + } else { + false + }; + if not_ready { + return None; + } + self.fut = None; + info.count -= 1; + + match info.mws[info.count as usize] + .finish(&mut info.req, self.resp.as_ref().unwrap()) + { + MiddlewareFinished::Done => { + if info.count == 0 { + return Some(Response::init(self.resp.take().unwrap())); + } + } + MiddlewareFinished::Future(fut) => { + self.fut = Some(fut); + } + } + } + } +} + +struct Response { + resp: Option, + _s: PhantomData, +} + +impl Response { + fn init(resp: HttpResponse) -> ComposeState { + ComposeState::Completed(Response { + resp: Some(resp), + _s: PhantomData, + }) + } +} + +#[cfg(test)] +mod tests { + use application::App; + use http::StatusCode; + use httpresponse::HttpResponse; + use test::TestRequest; + + #[test] + fn test_scope() { + let mut app = App::new() + .scope("/app", |scope| { + scope.resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + }) + .finish(); + + let req = TestRequest::with_uri("/app/path1").finish(); + let resp = app.run(req); + assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK); + } + + #[test] + fn test_default_resource() { + let mut app = App::new() + .scope("/app", |scope| { + scope + .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + .default_resource(|r| r.f(|_| HttpResponse::BadRequest())) + }) + .finish(); + + let req = TestRequest::with_uri("/app/path2").finish(); + let resp = app.run(req); + assert_eq!( + resp.as_response().unwrap().status(), + StatusCode::BAD_REQUEST + ); + + let req = TestRequest::with_uri("/path2").finish(); + let resp = app.run(req); + assert_eq!( + resp.as_response().unwrap().status(), + StatusCode::NOT_FOUND + ); + } +}