diff --git a/CHANGES.md b/CHANGES.md index 15ed9aead..549525547 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,9 @@ ## 0.4.11 -* Added `HttpReuqest::extract_xxx()`, type safe path/query information extractor. +* Added `Route::with()` handler, uses request extractor + +* Added `HttpReuqest::extract_xxx()`, type safe path/query information extractor * Fix long client urls #129 diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 3247e5d6c..5d074f935 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -30,6 +30,11 @@ fn index(req: HttpRequest) -> Box> { .responder() } +/// This handler uses `With` helper for loading serde json object. +fn extract_item(_: &HttpRequest, item: Json) -> Result { + println!("model: {:?}", &item); + httpcodes::HTTPOk.build().json(item.0) // <- send response +} const MAX_SIZE: usize = 262_144; // max payload size is 256k @@ -73,7 +78,6 @@ fn index_mjsonrust(req: HttpRequest) -> Box { - fn extract(&self, req: &'de HttpRequest) -> Result - where T: de::Deserialize<'de>, S: 'static; + +pub trait HttpRequestExtractor: Sized where T: DeserializeOwned +{ + type Result: Future; + + fn extract(req: &HttpRequest) -> Self::Result; } /// Extract typed information from the request's path. @@ -18,32 +25,49 @@ pub trait HttpRequestExtractor<'de> { /// # extern crate futures; /// #[macro_use] extern crate serde_derive; /// use actix_web::*; -/// use actix_web::dev::{Path, HttpRequestExtractor}; +/// use actix_web::Path; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// -/// fn index(mut req: HttpRequest) -> Result { -/// let info: Info = Path.extract(&req)?; // <- extract path info using serde +/// /// extract path info using serde +/// fn index(req: &HttpRequest, info: Path) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// /// fn main() { -/// let app = Application::new() -/// .resource("/{username}/index.html", // <- define path parameters -/// |r| r.method(Method::GET).f(index)); +/// let app = Application::new().resource( +/// "/{username}/index.html", // <- define path parameters +/// |r| r.method(Method::GET).with(index)); // <- use `with` extractor /// } /// ``` -pub struct Path; +pub struct Path(pub T); + +impl Deref for Path { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Path { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl HttpRequestExtractor for Path where T: DeserializeOwned +{ + type Result = FutureResult; -impl<'de> HttpRequestExtractor<'de> for Path { #[inline] - fn extract(&self, req: &'de HttpRequest) -> Result - where T: de::Deserialize<'de>, S: 'static, - { - de::Deserialize::deserialize(PathExtractor{req: req}) + fn extract(req: &HttpRequest) -> Self::Result { + result(de::Deserialize::deserialize(PathExtractor{req}) + .map_err(|e| e.into()) + .map(Path)) } } @@ -57,28 +81,50 @@ impl<'de> HttpRequestExtractor<'de> for Path { /// # extern crate futures; /// #[macro_use] extern crate serde_derive; /// use actix_web::*; -/// use actix_web::dev::{Query, HttpRequestExtractor}; +/// use actix_web::Query; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// -/// fn index(mut req: HttpRequest) -> Result { -/// let info: Info = Query.extract(&req)?; // <- extract query info using serde +/// // use `with` extractor for query info +/// // this handler get called only if request's query contains `username` field +/// fn index(req: &HttpRequest, info: Query) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// -/// # fn main() {} +/// fn main() { +/// let app = Application::new().resource( +/// "/index.html", +/// |r| r.method(Method::GET).with(index)); // <- use `with` extractor +/// } /// ``` -pub struct Query; +pub struct Query(pub T); + +impl Deref for Query { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Query { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl HttpRequestExtractor for Query where T: de::DeserializeOwned +{ + type Result = FutureResult; -impl<'de> HttpRequestExtractor<'de> for Query { #[inline] - fn extract(&self, req: &'de HttpRequest) -> Result - where T: de::Deserialize<'de>, S: 'static, - { - serde_urlencoded::from_str::(req.query_string()) + fn extract(req: &HttpRequest) -> Self::Result { + result(serde_urlencoded::from_str::(req.query_string()) + .map_err(|e| e.into()) + .map(Query)) } } @@ -92,11 +138,11 @@ macro_rules! unsupported_type { }; } -pub struct PathExtractor<'de, S: 'static> { +pub struct PathExtractor<'de, S: 'de> { req: &'de HttpRequest } -impl<'de, S: 'static> Deserializer<'de> for PathExtractor<'de, S> +impl<'de, S: 'de> Deserializer<'de> for PathExtractor<'de, S> { type Error = de::value::Error; diff --git a/src/handler.rs b/src/handler.rs index 7b8f2d480..e688a35f9 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -155,6 +155,14 @@ impl Reply { _ => None, } } + + #[cfg(test)] + pub(crate) fn into_future(self) -> Box> { + match self.0 { + ReplyItem::Future(fut) => fut, + _ => panic!(), + } + } } impl Responder for Reply { diff --git a/src/httprequest.rs b/src/httprequest.rs index 53f4e68fa..539f752fe 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::net::SocketAddr; use bytes::Bytes; use cookie::Cookie; -use futures::{Async, Stream, Poll}; +use futures::{Async, Future, Stream, Poll}; use futures_cpupool::CpuPool; use failure; use url::{Url, form_urlencoded}; @@ -426,11 +426,14 @@ impl HttpRequest { /// |r| r.method(Method::GET).f(index)); /// } /// ``` - pub fn extract_path<'a, T>(&'a self) -> Result + pub fn extract_path(&self) -> Result where S: 'static, - T: de::Deserialize<'a>, + T: de::DeserializeOwned, { - Ok(Path.extract(self)?) + match Path::::extract(self).poll()? { + Async::Ready(val) => Ok(val.0), + _ => unreachable!() + } } /// Extract typed information from request's query string. @@ -475,11 +478,14 @@ impl HttpRequest { /// # fn main() {} /// ``` /// - pub fn extract_query<'a, T>(&'a self) -> Result + pub fn extract_query(&self) -> Result where S: 'static, - T: de::Deserialize<'a>, + T: de::DeserializeOwned, { - Ok(Query.extract(self)?) + match Query::::extract(self).poll()? { + Async::Ready(val) => Ok(val.0), + _ => unreachable!() + } } /// Checks if a connection should be kept alive. diff --git a/src/json.rs b/src/json.rs index 04bf13d5f..5687e75c5 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,3 +1,4 @@ +use std::fmt; use bytes::{Bytes, BytesMut}; use futures::{Poll, Future, Stream}; use http::header::CONTENT_LENGTH; @@ -12,6 +13,7 @@ use handler::Responder; use httpmessage::HttpMessage; use httprequest::HttpRequest; use httpresponse::HttpResponse; +use extractor::HttpRequestExtractor; /// Json response helper /// @@ -34,7 +36,19 @@ use httpresponse::HttpResponse; /// } /// # fn main() {} /// ``` -pub struct Json (pub T); +pub struct Json(pub T); + +impl fmt::Debug for Json where T: fmt::Debug { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Json: {:?}", self.0) + } +} + +impl fmt::Display for Json where T: fmt::Display { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} impl Responder for Json { type Item = HttpResponse; @@ -49,6 +63,19 @@ impl Responder for Json { } } +impl HttpRequestExtractor for Json where T: DeserializeOwned + 'static +{ + type Result = Box>; + + #[inline] + fn extract(req: &HttpRequest) -> Self::Result { + Box::new( + JsonBody::new(req.clone()) + .from_err() + .map(Json)) + } +} + /// Request payload json parser that resolves to a deserialized `T` value. /// /// Returns error: @@ -160,6 +187,9 @@ mod tests { use http::header; use futures::Async; + use with::with; + use handler::Handler; + impl PartialEq for JsonPayloadError { fn eq(&self, other: &JsonPayloadError) -> bool { match *self { @@ -215,6 +245,25 @@ mod tests { header::HeaderValue::from_static("16")); req.payload_mut().unread_data(Bytes::from_static(b"{\"name\": \"test\"}")); let mut json = req.json::(); - assert_eq!(json.poll().ok().unwrap(), Async::Ready(MyObject{name: "test".to_owned()})); + assert_eq!(json.poll().ok().unwrap(), + Async::Ready(MyObject{name: "test".to_owned()})); + } + + #[test] + fn test_with_json() { + let mut handler = with(|_: &_, data: Json| data); + + let req = HttpRequest::default(); + let mut json = handler.handle(req).into_future(); + assert!(json.poll().is_err()); + + let mut req = HttpRequest::default(); + req.headers_mut().insert(header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json")); + req.headers_mut().insert(header::CONTENT_LENGTH, + header::HeaderValue::from_static("16")); + req.payload_mut().unread_data(Bytes::from_static(b"{\"name\": \"test\"}")); + let mut json = handler.handle(req).into_future(); + assert!(json.poll().is_ok()) } } diff --git a/src/lib.rs b/src/lib.rs index 2e92c44fd..60788e036 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,6 +119,7 @@ mod resource; mod param; mod payload; mod pipeline; +mod with; mod extractor; pub mod client; @@ -144,6 +145,7 @@ pub use route::Route; pub use resource::Resource; pub use context::HttpContext; pub use server::HttpServer; +pub use extractor::{Path, Query, HttpRequestExtractor}; // re-exports pub use http::{Method, StatusCode, Version}; @@ -184,10 +186,10 @@ pub mod dev { pub use context::Drain; pub use info::ConnectionInfo; pub use handler::Handler; + pub use with::With; pub use json::JsonBody; pub use router::{Router, Pattern}; pub use param::{FromParam, Params}; pub use httpmessage::{UrlEncoded, MessageBody}; pub use httpresponse::HttpResponseBuilder; - pub use extractor::{Path, Query, HttpRequestExtractor}; } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index f5f2e270b..a3d372820 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -233,14 +233,14 @@ impl FormatText { FormatText::Time => { let response_time = time::now() - entry_time; let response_time = response_time.num_seconds() as f64 + - (response_time.num_nanoseconds().unwrap_or(0) as f64) / 1000000000.0; + (response_time.num_nanoseconds().unwrap_or(0) as f64)/1_000_000_000.0; fmt.write_fmt(format_args!("{:.6}", response_time)) }, FormatText::TimeMillis => { let response_time = time::now() - entry_time; let response_time_ms = (response_time.num_seconds() * 1000) as f64 + - (response_time.num_nanoseconds().unwrap_or(0) as f64) / 1000000.0; + (response_time.num_nanoseconds().unwrap_or(0) as f64)/1_000_000.0; fmt.write_fmt(format_args!("{:.6}", response_time_ms)) }, diff --git a/src/resource.rs b/src/resource.rs index 3d8c4b982..732d788cc 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use smallvec::SmallVec; use http::{Method, StatusCode}; +use serde::de::DeserializeOwned; use pred; use body::Body; @@ -11,6 +12,8 @@ use handler::{Reply, Handler, Responder}; use middleware::Middleware; use httprequest::HttpRequest; use httpresponse::HttpResponse; +use with::WithHandler; +use extractor::HttpRequestExtractor; /// *Resource* is an entry in route table which corresponds to requested URL. /// @@ -132,6 +135,22 @@ impl Resource { self.routes.last_mut().unwrap().f(handler) } + /// Register a new route and add handler. + /// + /// This is shortcut for: + /// + /// ```rust,ignore + /// Resource::resource("/", |r| r.route().with(index) + /// ``` + pub fn with(&mut self, handler: H) + where H: WithHandler, + D: HttpRequestExtractor + 'static, + T: DeserializeOwned + 'static, + { + self.routes.push(Route::default()); + self.routes.last_mut().unwrap().with(handler) + } + /// Register a middleware /// /// This is similar to `Application's` middlewares, but diff --git a/src/route.rs b/src/route.rs index d57f1c332..a5dd1c431 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,6 +1,7 @@ use std::mem; use std::rc::Rc; use std::marker::PhantomData; +use serde::de::DeserializeOwned; use futures::{Async, Future, Poll}; use error::Error; @@ -11,6 +12,8 @@ use middleware::{Middleware, Response as MiddlewareResponse, Started as Middlewa use httpcodes::HttpNotFound; use httprequest::HttpRequest; use httpresponse::HttpResponse; +use with::{with, WithHandler}; +use extractor::HttpRequestExtractor; /// Resource route definition /// @@ -107,6 +110,40 @@ impl Route { { self.handler = InnerHandler::async(handler); } + + /// Set handler function with http request extractor. + /// + /// ```rust + /// # extern crate bytes; + /// # extern crate actix_web; + /// # extern crate futures; + /// #[macro_use] extern crate serde_derive; + /// use actix_web::*; + /// use actix_web::{with, Path, HttpRequestExtractor}; + /// + /// #[derive(Deserialize)] + /// struct Info { + /// username: String, + /// } + /// + /// /// extract path info using serde + /// fn index(req: &HttpRequest, info: Path) -> Result { + /// Ok(format!("Welcome {}!", info.username)) + /// } + /// + /// fn main() { + /// let app = Application::new().resource( + /// "/{username}/index.html", // <- define path parameters + /// |r| r.method(Method::GET).with(index)); // <- use `with` extractor + /// } + /// ``` + pub fn with(&mut self, handler: H) + where H: WithHandler, + D: HttpRequestExtractor + 'static, + T: DeserializeOwned + 'static, + { + self.h(with(handler)) + } } /// `RouteHandler` wrapper. This struct is required because it needs to be shared diff --git a/src/with.rs b/src/with.rs new file mode 100644 index 000000000..8e1f839f0 --- /dev/null +++ b/src/with.rs @@ -0,0 +1,130 @@ +use std::rc::Rc; +use std::cell::UnsafeCell; +use std::marker::PhantomData; +use serde::de::DeserializeOwned; +use futures::{Async, Future, Poll}; + +use error::Error; +use handler::{Handler, Reply, ReplyItem, Responder}; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use extractor::HttpRequestExtractor; + + +/// Trait defines object that could be registered as route handler +#[allow(unused_variables)] +pub trait WithHandler: 'static + where D: HttpRequestExtractor, T: DeserializeOwned +{ + /// The type of value that handler will return. + type Result: Responder; + + /// Handle request + fn handle(&mut self, req: &HttpRequest, data: D) -> Self::Result; +} + +/// WithHandler for Fn() +impl WithHandler for F + where F: Fn(&HttpRequest, D) -> R + 'static, + R: Responder + 'static, + D: HttpRequestExtractor, + T: DeserializeOwned, +{ + type Result = R; + + fn handle(&mut self, req: &HttpRequest, item: D) -> R { + (self)(req, item) + } +} + +pub fn with(h: H) -> With + where H: WithHandler, + D: HttpRequestExtractor, + T: DeserializeOwned, +{ + With{hnd: Rc::new(UnsafeCell::new(h)), + _t: PhantomData, _d: PhantomData, _s: PhantomData} +} + +pub struct With + where H: WithHandler, + D: HttpRequestExtractor, + T: DeserializeOwned, +{ + hnd: Rc>, + _t: PhantomData, + _d: PhantomData, + _s: PhantomData, +} + +impl Handler for With + where H: WithHandler, + D: HttpRequestExtractor, + T: DeserializeOwned, + T: 'static, D: 'static, S: 'static +{ + type Result = Reply; + + fn handle(&mut self, req: HttpRequest) -> Self::Result { + let fut = Box::new(D::extract(&req)); + + Reply::async( + WithHandlerFut{ + req, + hnd: Rc::clone(&self.hnd), + fut1: Some(fut), + fut2: None, + _t: PhantomData, + _d: PhantomData, + }) + } +} + +struct WithHandlerFut + where H: WithHandler, + D: HttpRequestExtractor, + T: DeserializeOwned, + T: 'static, D: 'static, S: 'static +{ + hnd: Rc>, + req: HttpRequest, + fut1: Option>>, + fut2: Option>>, + _t: PhantomData, + _d: PhantomData, +} + +impl Future for WithHandlerFut + where H: WithHandler, + D: HttpRequestExtractor, + T: DeserializeOwned, + T: 'static, D: 'static, S: 'static +{ + type Item = HttpResponse; + type Error = Error; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut2 { + return fut.poll() + } + + let item = match self.fut1.as_mut().unwrap().poll()? { + Async::Ready(item) => item, + Async::NotReady => return Ok(Async::NotReady), + }; + + let hnd: &mut H = unsafe{&mut *self.hnd.get()}; + let item = match hnd.handle(&self.req, item).respond_to(self.req.without_state()) + { + Ok(item) => item.into(), + Err(err) => return Err(err.into()), + }; + + match item.into() { + ReplyItem::Message(resp) => return Ok(Async::Ready(resp)), + ReplyItem::Future(fut) => self.fut2 = Some(fut), + } + + self.poll() + } +}