From 18575ee1ee26bfc54bea891d452b0a51f4b42b73 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 9 May 2018 16:27:31 -0700 Subject: [PATCH] Add Router::with_async() method for async handler registration --- CHANGES.md | 5 ++ build.rs | 11 +++- src/resource.rs | 21 +++++++ src/route.rs | 72 ++++++++++++++++++++- src/with.rs | 139 +++++++++++++++++++++++++++++++++++++++++ tests/test_handlers.rs | 78 +++++++++++++++++++++++ 6 files changed, 324 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 51267c763..cf4df3e36 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## 0.6.3 (2018-05-xx) + +* Add `Router::with_async()` method for async handler registration. + + ## 0.6.2 (2018-05-09) * WsWriter trait is optional. diff --git a/build.rs b/build.rs index 3b3001f90..7cb25c731 100644 --- a/build.rs +++ b/build.rs @@ -1,8 +1,17 @@ extern crate version_check; fn main() { + let mut has_impl_trait = true; + + match version_check::is_min_version("1.26.0") { + Some((true, _)) => println!("cargo:rustc-cfg=actix_impl_trait"), + _ => (), + }; match version_check::is_nightly() { - Some(true) => println!("cargo:rustc-cfg=actix_nightly"), + Some(true) => { + println!("cargo:rustc-cfg=actix_nightly"); + println!("cargo:rustc-cfg=actix_impl_trait"); + } Some(false) => (), None => (), }; diff --git a/src/resource.rs b/src/resource.rs index fb08afd94..e52760f4e 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,9 +1,11 @@ use std::marker::PhantomData; use std::rc::Rc; +use futures::Future; use http::{Method, StatusCode}; use smallvec::SmallVec; +use error::Error; use handler::{AsyncResult, FromRequest, Handler, Responder}; use httprequest::HttpRequest; use httpresponse::HttpResponse; @@ -183,6 +185,25 @@ impl ResourceHandler { self.routes.last_mut().unwrap().with(handler); } + /// Register a new route and add async handler. + /// + /// This is shortcut for: + /// + /// ```rust,ignore + /// Application::resource("/", |r| r.route().with_async(index) + /// ``` + pub fn with_async(&mut self, handler: F) + where + F: Fn(T) -> R + 'static, + R: Future + 'static, + I: Responder + 'static, + E: Into + 'static, + T: FromRequest + 'static, + { + self.routes.push(Route::default()); + self.routes.last_mut().unwrap().with_async(handler); + } + /// Register a resource middleware /// /// This is similar to `App's` middlewares, but diff --git a/src/route.rs b/src/route.rs index 215a7f226..4ff3279eb 100644 --- a/src/route.rs +++ b/src/route.rs @@ -13,7 +13,7 @@ use httpresponse::HttpResponse; use middleware::{Finished as MiddlewareFinished, Middleware, Response as MiddlewareResponse, Started as MiddlewareStarted}; use pred::Predicate; -use with::{ExtractorConfig, With, With2, With3}; +use with::{ExtractorConfig, With, With2, With3, WithAsync}; /// Resource route definition /// @@ -129,6 +129,34 @@ impl Route { /// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor /// } /// ``` + /// + /// It is possible to use tuples for specifing multiple extractors for one + /// handler function. + /// + /// ```rust + /// # extern crate bytes; + /// # extern crate actix_web; + /// # extern crate futures; + /// #[macro_use] extern crate serde_derive; + /// # use std::collections::HashMap; + /// use actix_web::{http, App, Query, Path, Result, Json}; + /// + /// #[derive(Deserialize)] + /// struct Info { + /// username: String, + /// } + /// + /// /// extract path info using serde + /// fn index(info: (Path, Query>, Json)) -> Result { + /// Ok(format!("Welcome {}!", info.0.username)) + /// } + /// + /// fn main() { + /// let app = App::new().resource( + /// "/{username}/index.html", // <- define path parameters + /// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor + /// } + /// ``` pub fn with(&mut self, handler: F) -> ExtractorConfig where F: Fn(T) -> R + 'static, @@ -140,6 +168,47 @@ impl Route { cfg } + /// Set async handler function, use request extractor for parameters. + /// + /// ```rust + /// # extern crate bytes; + /// # extern crate actix_web; + /// # extern crate futures; + /// #[macro_use] extern crate serde_derive; + /// use actix_web::{App, Path, Error, http}; + /// use futures::Future; + /// + /// #[derive(Deserialize)] + /// struct Info { + /// username: String, + /// } + /// + /// /// extract path info using serde + /// fn index(info: Path) -> Box> { + /// unimplemented!() + /// } + /// + /// fn main() { + /// let app = App::new().resource( + /// "/{username}/index.html", // <- define path parameters + /// |r| r.method(http::Method::GET) + /// .with_async(index)); // <- use `with` extractor + /// } + /// ``` + pub fn with_async(&mut self, handler: F) -> ExtractorConfig + where + F: Fn(T) -> R + 'static, + R: Future + 'static, + I: Responder + 'static, + E: Into + 'static, + T: FromRequest + 'static, + { + let cfg = ExtractorConfig::default(); + self.h(WithAsync::new(handler, Clone::clone(&cfg))); + cfg + } + + #[doc(hidden)] /// Set handler function, use request extractor for both parameters. /// /// ```rust @@ -189,6 +258,7 @@ impl Route { (cfg1, cfg2) } + #[doc(hidden)] /// Set handler function, use request extractor for all parameters. pub fn with3( &mut self, handler: F, diff --git a/src/with.rs b/src/with.rs index fa3d7d802..dca600bbe 100644 --- a/src/with.rs +++ b/src/with.rs @@ -167,6 +167,145 @@ where } } +pub struct WithAsync +where + F: Fn(T) -> R, + R: Future, + I: Responder, + E: Into, + T: FromRequest, + S: 'static, +{ + hnd: Rc>, + cfg: ExtractorConfig, + _s: PhantomData, +} + +impl WithAsync +where + F: Fn(T) -> R, + R: Future, + I: Responder, + E: Into, + T: FromRequest, + S: 'static, +{ + pub fn new(f: F, cfg: ExtractorConfig) -> Self { + WithAsync { + cfg, + hnd: Rc::new(UnsafeCell::new(f)), + _s: PhantomData, + } + } +} + +impl Handler for WithAsync +where + F: Fn(T) -> R + 'static, + R: Future + 'static, + I: Responder + 'static, + E: Into + 'static, + T: FromRequest + 'static, + S: 'static, +{ + type Result = AsyncResult; + + fn handle(&mut self, req: HttpRequest) -> Self::Result { + let mut fut = WithAsyncHandlerFut { + req, + started: false, + hnd: Rc::clone(&self.hnd), + cfg: self.cfg.clone(), + fut1: None, + fut2: None, + fut3: None, + }; + + match fut.poll() { + Ok(Async::Ready(resp)) => AsyncResult::ok(resp), + Ok(Async::NotReady) => AsyncResult::async(Box::new(fut)), + Err(e) => AsyncResult::err(e), + } + } +} + +struct WithAsyncHandlerFut +where + F: Fn(T) -> R, + R: Future + 'static, + I: Responder + 'static, + E: Into + 'static, + T: FromRequest + 'static, + S: 'static, +{ + started: bool, + hnd: Rc>, + cfg: ExtractorConfig, + req: HttpRequest, + fut1: Option>>, + fut2: Option, + fut3: Option>>, +} + +impl Future for WithAsyncHandlerFut +where + F: Fn(T) -> R, + R: Future + 'static, + I: Responder + 'static, + E: Into + 'static, + T: FromRequest + 'static, + S: 'static, +{ + type Item = HttpResponse; + type Error = Error; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut3 { + return fut.poll(); + } + + if self.fut2.is_some() { + return match self.fut2.as_mut().unwrap().poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(r)) => match r.respond_to(&self.req) { + Ok(r) => match r.into().into() { + AsyncResultItem::Err(err) => Err(err), + AsyncResultItem::Ok(resp) => Ok(Async::Ready(resp)), + AsyncResultItem::Future(fut) => { + self.fut3 = Some(fut); + self.poll() + } + }, + Err(e) => Err(e.into()), + }, + Err(e) => Err(e.into()), + }; + } + + let item = if !self.started { + self.started = true; + let reply = T::from_request(&self.req, self.cfg.as_ref()).into(); + match reply.into() { + AsyncResultItem::Err(err) => return Err(err), + AsyncResultItem::Ok(msg) => msg, + AsyncResultItem::Future(fut) => { + self.fut1 = Some(fut); + return self.poll(); + } + } + } else { + match self.fut1.as_mut().unwrap().poll()? { + Async::Ready(item) => item, + Async::NotReady => return Ok(Async::NotReady), + } + }; + + let hnd: &mut F = unsafe { &mut *self.hnd.get() }; + self.fut2 = Some((*hnd)(item)); + self.poll() + } +} + pub struct With2 where F: Fn(T1, T2) -> R, diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs index 8aea34d0a..42a9f3ace 100644 --- a/tests/test_handlers.rs +++ b/tests/test_handlers.rs @@ -9,6 +9,7 @@ extern crate tokio_core; extern crate serde_derive; extern crate serde_json; +use std::io; use std::time::Duration; use actix::*; @@ -377,6 +378,83 @@ fn test_path_and_query_extractor2_async4() { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } +#[cfg(actix_impl_trait)] +fn test_impl_trait( + data: (Json, Path, Query), +) -> impl Future { + Timeout::new(Duration::from_millis(10), &Arbiter::handle()) + .unwrap() + .and_then(move |_| { + Ok(format!( + "Welcome {} - {}!", + data.1.username, + (data.0).0 + )) + }) +} + +#[cfg(actix_impl_trait)] +fn test_impl_trait_err( + _data: (Json, Path, Query), +) -> impl Future { + Timeout::new(Duration::from_millis(10), &Arbiter::handle()) + .unwrap() + .and_then(move |_| Err(io::Error::new(io::ErrorKind::Other, "other"))) +} + +#[cfg(actix_impl_trait)] +#[test] +fn test_path_and_query_extractor2_async4_impl_trait() { + let mut srv = test::TestServer::new(|app| { + app.resource("/{username}/index.html", |r| { + r.route().with_async(test_impl_trait) + }); + }); + + // client request + let request = srv.post() + .uri(srv.url("/test1/index.html?username=test2")) + .header("content-type", "application/json") + .body("{\"test\": 1}") + .unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.execute(response.body()).unwrap(); + assert_eq!( + bytes, + Bytes::from_static(b"Welcome test1 - {\"test\":1}!") + ); + + // client request + let request = srv.get() + .uri(srv.url("/test1/index.html")) + .finish() + .unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); +} + +#[cfg(actix_impl_trait)] +#[test] +fn test_path_and_query_extractor2_async4_impl_trait_err() { + let mut srv = test::TestServer::new(|app| { + app.resource("/{username}/index.html", |r| { + r.route().with_async(test_impl_trait_err) + }); + }); + + // client request + let request = srv.post() + .uri(srv.url("/test1/index.html?username=test2")) + .header("content-type", "application/json") + .body("{\"test\": 1}") + .unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); +} + #[test] fn test_non_ascii_route() { let mut srv = test::TestServer::new(|app| {