diff --git a/CHANGES.md b/CHANGES.md index 267d7a4b3..29cc1ae6f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## 0.5.0 -* Type-safe path/query parameter handling, using serde #70 +* Type-safe path/query/form parameter handling, using serde #70 * HttpResponse builder's methods `.body()`, `.finish()`, `.json()` return `HttpResponse` instead of `Result` diff --git a/src/de.rs b/src/de.rs index a72d6b5b4..637385a32 100644 --- a/src/de.rs +++ b/src/de.rs @@ -88,7 +88,7 @@ impl DerefMut for Path { } impl Path { - /// Deconstruct to a inner value + /// Deconstruct to an inner value pub fn into_inner(self) -> T { self.inner } diff --git a/src/handler.rs b/src/handler.rs index 855df5353..08e49797b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -373,7 +373,7 @@ impl RouteHandler for AsyncHandler } } -/// Access to an application state +/// Access an application state /// /// `S` - application state type /// diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 47b42dc91..32ccb39f6 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -1,20 +1,23 @@ use std::str; -use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; use bytes::{Bytes, BytesMut}; use futures::{Future, Stream, Poll}; use http_range::HttpRange; use serde::de::DeserializeOwned; use mime::Mime; -use url::form_urlencoded; +use serde_urlencoded; use encoding::all::UTF_8; use encoding::EncodingRef; +use encoding::types::{Encoding, DecoderTrap}; use encoding::label::encoding_from_whatwg_label; use http::{header, HeaderMap}; use json::JsonBody; use header::Header; +use handler::FromRequest; use multipart::Multipart; -use error::{ParseError, ContentTypeError, +use httprequest::HttpRequest; +use error::{Error, ParseError, ContentTypeError, HttpRangeError, PayloadError, UrlencodedError}; @@ -137,8 +140,8 @@ pub trait HttpMessage { } /// Parse `application/x-www-form-urlencoded` encoded request's body. - /// Return `UrlEncoded` future. It resolves to a `HashMap` which - /// contains decoded parameters. + /// Return `UrlEncoded` future. Form can be deserialized to any type that implements + /// `Deserialize` trait from *serde*. /// /// Returns error: /// @@ -152,20 +155,21 @@ pub trait HttpMessage { /// # extern crate actix_web; /// # extern crate futures; /// # use futures::Future; - /// use actix_web::*; + /// # use std::collections::HashMap; + /// use actix_web::{HttpMessage, HttpRequest, HttpResponse, FutureResponse}; /// /// fn index(mut req: HttpRequest) -> FutureResponse { - /// req.urlencoded() // <- get UrlEncoded future - /// .from_err() - /// .and_then(|params| { // <- url encoded parameters - /// println!("==== BODY ==== {:?}", params); - /// Ok(HttpResponse::Ok().into()) - /// }) - /// .responder() + /// Box::new( + /// req.urlencoded::>() // <- get UrlEncoded future + /// .from_err() + /// .and_then(|params| { // <- url encoded parameters + /// println!("==== BODY ==== {:?}", params); + /// Ok(HttpResponse::Ok().into()) + /// })) /// } /// # fn main() {} /// ``` - fn urlencoded(self) -> UrlEncoded + fn urlencoded(self) -> UrlEncoded where Self: Stream + Sized { UrlEncoded::new(self) @@ -321,14 +325,14 @@ impl Future for MessageBody } /// Future that resolves to a parsed urlencoded values. -pub struct UrlEncoded { +pub struct UrlEncoded { req: Option, limit: usize, - fut: Option, Error=UrlencodedError>>>, + fut: Option>>, } -impl UrlEncoded { - pub fn new(req: T) -> UrlEncoded { +impl UrlEncoded { + pub fn new(req: T) -> UrlEncoded { UrlEncoded { req: Some(req), limit: 262_144, @@ -343,10 +347,11 @@ impl UrlEncoded { } } -impl Future for UrlEncoded - where T: HttpMessage + Stream + 'static +impl Future for UrlEncoded + where T: HttpMessage + Stream + 'static, + U: DeserializeOwned + 'static { - type Item = HashMap; + type Item = U; type Error = UrlencodedError; fn poll(&mut self) -> Poll { @@ -385,13 +390,16 @@ impl Future for UrlEncoded } }) .and_then(move |body| { - let mut m = HashMap::new(); - let parsed = form_urlencoded::parse_with_encoding( - &body, Some(encoding), false).map_err(|_| UrlencodedError::Parse)?; - for (k, v) in parsed { - m.insert(k.into(), v.into()); + let enc: *const Encoding = encoding as *const Encoding; + if enc == UTF_8 { + serde_urlencoded::from_bytes::(&body) + .map_err(|_| UrlencodedError::Parse) + } else { + let body = encoding.decode(&body, DecoderTrap::Strict) + .map_err(|_| UrlencodedError::Parse)?; + serde_urlencoded::from_str::(&body) + .map_err(|_| UrlencodedError::Parse) } - Ok(m) }); self.fut = Some(Box::new(fut)); } @@ -400,6 +408,61 @@ impl Future for UrlEncoded } } +/// Extract typed information from the request's body. +/// +/// To extract typed information from request's body, the type `T` must implement the +/// `Deserialize` trait from *serde*. +/// +/// ## Example +/// +/// It is possible to extract path information to a specific type that implements +/// `Deserialize` trait from *serde*. +/// +/// ```rust +/// # extern crate actix_web; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{App, Form, Result}; +/// +/// #[derive(Deserialize)] +/// struct FormData { +/// username: String, +/// } +/// +/// /// extract form data using serde +/// /// this handle get called only if content type is *x-www-form-urlencoded* +/// /// and content of the request could be deserialized to a `FormData` struct +/// fn index(form: Form) -> Result { +/// Ok(format!("Welcome {}!", form.username)) +/// } +/// # fn main() {} +/// ``` +pub struct Form(pub T); + +impl Deref for Form { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Form { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl FromRequest for Form + where T: DeserializeOwned + 'static, S: 'static +{ + type Result = Box>; + + #[inline] + fn from_request(req: &HttpRequest) -> Self::Result { + Box::new(UrlEncoded::new(req.clone()).from_err().map(Form)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -410,7 +473,6 @@ mod tests { use http::{Method, Version, Uri}; use httprequest::HttpRequest; use std::str::FromStr; - use std::iter::FromIterator; use test::TestRequest; #[test] @@ -529,28 +591,37 @@ mod tests { } } + #[derive(Deserialize, Debug, PartialEq)] + struct Info { + hello: String, + } + #[test] fn test_urlencoded_error() { let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); - assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::Chunked); + assert_eq!(req.urlencoded::() + .poll().err().unwrap(), UrlencodedError::Chunked); let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded") .header(header::CONTENT_LENGTH, "xxxx") .finish(); - assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::UnknownLength); + assert_eq!(req.urlencoded::() + .poll().err().unwrap(), UrlencodedError::UnknownLength); let req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded") .header(header::CONTENT_LENGTH, "1000000") .finish(); - assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::Overflow); + assert_eq!(req.urlencoded::() + .poll().err().unwrap(), UrlencodedError::Overflow); let req = TestRequest::with_header( header::CONTENT_TYPE, "text/plain") .header(header::CONTENT_LENGTH, "10") .finish(); - assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::ContentType); + assert_eq!(req.urlencoded::() + .poll().err().unwrap(), UrlencodedError::ContentType); } #[test] @@ -561,9 +632,8 @@ mod tests { .finish(); req.payload_mut().unread_data(Bytes::from_static(b"hello=world")); - let result = req.urlencoded().poll().ok().unwrap(); - assert_eq!(result, Async::Ready( - HashMap::from_iter(vec![("hello".to_owned(), "world".to_owned())]))); + let result = req.urlencoded::().poll().ok().unwrap(); + assert_eq!(result, Async::Ready(Info{hello: "world".to_owned()})); let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8") @@ -572,8 +642,23 @@ mod tests { req.payload_mut().unread_data(Bytes::from_static(b"hello=world")); let result = req.urlencoded().poll().ok().unwrap(); - assert_eq!(result, Async::Ready( - HashMap::from_iter(vec![("hello".to_owned(), "world".to_owned())]))); + assert_eq!(result, Async::Ready(Info{hello: "world".to_owned()})); + } + + #[test] + fn test_urlencoded_extractor() { + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(header::CONTENT_LENGTH, "11") + .finish(); + req.payload_mut().unread_data(Bytes::from_static(b"hello=world")); + + match Form::::from_request(&req).poll().unwrap() { + Async::Ready(s) => { + assert_eq!(s.hello, "world"); + }, + _ => unreachable!(), + } } #[test] diff --git a/src/json.rs b/src/json.rs index ee2c1b80e..63e58fea8 100644 --- a/src/json.rs +++ b/src/json.rs @@ -56,7 +56,7 @@ use httpresponse::HttpResponse; /// username: String, /// } /// -/// /// extract `Info` using serde +/// /// deserialize `Info` from request's body /// fn index(info: Json) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } @@ -129,7 +129,6 @@ impl FromRequest for Json /// * content type is not `application/json` /// * content length is greater than 256k /// -/// /// # Server example /// /// ```rust diff --git a/src/lib.rs b/src/lib.rs index 6d544d822..70a61d747 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,7 +139,7 @@ pub use body::{Body, Binary}; pub use json::Json; pub use de::{Path, Query}; pub use application::App; -pub use httpmessage::HttpMessage; +pub use httpmessage::{HttpMessage, Form}; pub use httprequest::HttpRequest; pub use httpresponse::HttpResponse; pub use handler::{Either, Responder, AsyncResponder, FutureResponse, State};