1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-24 16:02:59 +01:00

add urlencoded body extractor

This commit is contained in:
Nikolay Kim 2018-04-02 14:00:18 -07:00
parent 280c8d87f8
commit cbf4c61eb5
6 changed files with 127 additions and 43 deletions

View File

@ -2,7 +2,7 @@
## 0.5.0 ## 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()` * HttpResponse builder's methods `.body()`, `.finish()`, `.json()`
return `HttpResponse` instead of `Result` return `HttpResponse` instead of `Result`

View File

@ -88,7 +88,7 @@ impl<T> DerefMut for Path<T> {
} }
impl<T> Path<T> { impl<T> Path<T> {
/// Deconstruct to a inner value /// Deconstruct to an inner value
pub fn into_inner(self) -> T { pub fn into_inner(self) -> T {
self.inner self.inner
} }

View File

@ -373,7 +373,7 @@ impl<S, H, F, R, E> RouteHandler<S> for AsyncHandler<S, H, F, R, E>
} }
} }
/// Access to an application state /// Access an application state
/// ///
/// `S` - application state type /// `S` - application state type
/// ///

View File

@ -1,20 +1,23 @@
use std::str; use std::str;
use std::collections::HashMap; use std::ops::{Deref, DerefMut};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures::{Future, Stream, Poll}; use futures::{Future, Stream, Poll};
use http_range::HttpRange; use http_range::HttpRange;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use mime::Mime; use mime::Mime;
use url::form_urlencoded; use serde_urlencoded;
use encoding::all::UTF_8; use encoding::all::UTF_8;
use encoding::EncodingRef; use encoding::EncodingRef;
use encoding::types::{Encoding, DecoderTrap};
use encoding::label::encoding_from_whatwg_label; use encoding::label::encoding_from_whatwg_label;
use http::{header, HeaderMap}; use http::{header, HeaderMap};
use json::JsonBody; use json::JsonBody;
use header::Header; use header::Header;
use handler::FromRequest;
use multipart::Multipart; use multipart::Multipart;
use error::{ParseError, ContentTypeError, use httprequest::HttpRequest;
use error::{Error, ParseError, ContentTypeError,
HttpRangeError, PayloadError, UrlencodedError}; HttpRangeError, PayloadError, UrlencodedError};
@ -137,8 +140,8 @@ pub trait HttpMessage {
} }
/// Parse `application/x-www-form-urlencoded` encoded request's body. /// Parse `application/x-www-form-urlencoded` encoded request's body.
/// Return `UrlEncoded` future. It resolves to a `HashMap<String, String>` which /// Return `UrlEncoded` future. Form can be deserialized to any type that implements
/// contains decoded parameters. /// `Deserialize` trait from *serde*.
/// ///
/// Returns error: /// Returns error:
/// ///
@ -152,20 +155,21 @@ pub trait HttpMessage {
/// # extern crate actix_web; /// # extern crate actix_web;
/// # extern crate futures; /// # extern crate futures;
/// # use futures::Future; /// # use futures::Future;
/// use actix_web::*; /// # use std::collections::HashMap;
/// use actix_web::{HttpMessage, HttpRequest, HttpResponse, FutureResponse};
/// ///
/// fn index(mut req: HttpRequest) -> FutureResponse<HttpResponse> { /// fn index(mut req: HttpRequest) -> FutureResponse<HttpResponse> {
/// req.urlencoded() // <- get UrlEncoded future /// Box::new(
/// req.urlencoded::<HashMap<String, String>>() // <- get UrlEncoded future
/// .from_err() /// .from_err()
/// .and_then(|params| { // <- url encoded parameters /// .and_then(|params| { // <- url encoded parameters
/// println!("==== BODY ==== {:?}", params); /// println!("==== BODY ==== {:?}", params);
/// Ok(HttpResponse::Ok().into()) /// Ok(HttpResponse::Ok().into())
/// }) /// }))
/// .responder()
/// } /// }
/// # fn main() {} /// # fn main() {}
/// ``` /// ```
fn urlencoded(self) -> UrlEncoded<Self> fn urlencoded<T: DeserializeOwned>(self) -> UrlEncoded<Self, T>
where Self: Stream<Item=Bytes, Error=PayloadError> + Sized where Self: Stream<Item=Bytes, Error=PayloadError> + Sized
{ {
UrlEncoded::new(self) UrlEncoded::new(self)
@ -321,14 +325,14 @@ impl<T> Future for MessageBody<T>
} }
/// Future that resolves to a parsed urlencoded values. /// Future that resolves to a parsed urlencoded values.
pub struct UrlEncoded<T> { pub struct UrlEncoded<T, U> {
req: Option<T>, req: Option<T>,
limit: usize, limit: usize,
fut: Option<Box<Future<Item=HashMap<String, String>, Error=UrlencodedError>>>, fut: Option<Box<Future<Item=U, Error=UrlencodedError>>>,
} }
impl<T> UrlEncoded<T> { impl<T, U> UrlEncoded<T, U> {
pub fn new(req: T) -> UrlEncoded<T> { pub fn new(req: T) -> UrlEncoded<T, U> {
UrlEncoded { UrlEncoded {
req: Some(req), req: Some(req),
limit: 262_144, limit: 262_144,
@ -343,10 +347,11 @@ impl<T> UrlEncoded<T> {
} }
} }
impl<T> Future for UrlEncoded<T> impl<T, U> Future for UrlEncoded<T, U>
where T: HttpMessage + Stream<Item=Bytes, Error=PayloadError> + 'static where T: HttpMessage + Stream<Item=Bytes, Error=PayloadError> + 'static,
U: DeserializeOwned + 'static
{ {
type Item = HashMap<String, String>; type Item = U;
type Error = UrlencodedError; type Error = UrlencodedError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
@ -385,13 +390,16 @@ impl<T> Future for UrlEncoded<T>
} }
}) })
.and_then(move |body| { .and_then(move |body| {
let mut m = HashMap::new(); let enc: *const Encoding = encoding as *const Encoding;
let parsed = form_urlencoded::parse_with_encoding( if enc == UTF_8 {
&body, Some(encoding), false).map_err(|_| UrlencodedError::Parse)?; serde_urlencoded::from_bytes::<U>(&body)
for (k, v) in parsed { .map_err(|_| UrlencodedError::Parse)
m.insert(k.into(), v.into()); } else {
let body = encoding.decode(&body, DecoderTrap::Strict)
.map_err(|_| UrlencodedError::Parse)?;
serde_urlencoded::from_str::<U>(&body)
.map_err(|_| UrlencodedError::Parse)
} }
Ok(m)
}); });
self.fut = Some(Box::new(fut)); self.fut = Some(Box::new(fut));
} }
@ -400,6 +408,61 @@ impl<T> Future for UrlEncoded<T>
} }
} }
/// 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<FormData>) -> Result<String> {
/// Ok(format!("Welcome {}!", form.username))
/// }
/// # fn main() {}
/// ```
pub struct Form<T>(pub T);
impl<T> Deref for Form<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> DerefMut for Form<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T, S> FromRequest<S> for Form<T>
where T: DeserializeOwned + 'static, S: 'static
{
type Result = Box<Future<Item=Self, Error=Error>>;
#[inline]
fn from_request(req: &HttpRequest<S>) -> Self::Result {
Box::new(UrlEncoded::new(req.clone()).from_err().map(Form))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -410,7 +473,6 @@ mod tests {
use http::{Method, Version, Uri}; use http::{Method, Version, Uri};
use httprequest::HttpRequest; use httprequest::HttpRequest;
use std::str::FromStr; use std::str::FromStr;
use std::iter::FromIterator;
use test::TestRequest; use test::TestRequest;
#[test] #[test]
@ -529,28 +591,37 @@ mod tests {
} }
} }
#[derive(Deserialize, Debug, PartialEq)]
struct Info {
hello: String,
}
#[test] #[test]
fn test_urlencoded_error() { fn test_urlencoded_error() {
let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish();
assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::Chunked); assert_eq!(req.urlencoded::<Info>()
.poll().err().unwrap(), UrlencodedError::Chunked);
let req = TestRequest::with_header( let req = TestRequest::with_header(
header::CONTENT_TYPE, "application/x-www-form-urlencoded") header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::CONTENT_LENGTH, "xxxx") .header(header::CONTENT_LENGTH, "xxxx")
.finish(); .finish();
assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::UnknownLength); assert_eq!(req.urlencoded::<Info>()
.poll().err().unwrap(), UrlencodedError::UnknownLength);
let req = TestRequest::with_header( let req = TestRequest::with_header(
header::CONTENT_TYPE, "application/x-www-form-urlencoded") header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::CONTENT_LENGTH, "1000000") .header(header::CONTENT_LENGTH, "1000000")
.finish(); .finish();
assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::Overflow); assert_eq!(req.urlencoded::<Info>()
.poll().err().unwrap(), UrlencodedError::Overflow);
let req = TestRequest::with_header( let req = TestRequest::with_header(
header::CONTENT_TYPE, "text/plain") header::CONTENT_TYPE, "text/plain")
.header(header::CONTENT_LENGTH, "10") .header(header::CONTENT_LENGTH, "10")
.finish(); .finish();
assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::ContentType); assert_eq!(req.urlencoded::<Info>()
.poll().err().unwrap(), UrlencodedError::ContentType);
} }
#[test] #[test]
@ -561,9 +632,8 @@ mod tests {
.finish(); .finish();
req.payload_mut().unread_data(Bytes::from_static(b"hello=world")); req.payload_mut().unread_data(Bytes::from_static(b"hello=world"));
let result = req.urlencoded().poll().ok().unwrap(); let result = req.urlencoded::<Info>().poll().ok().unwrap();
assert_eq!(result, Async::Ready( assert_eq!(result, Async::Ready(Info{hello: "world".to_owned()}));
HashMap::from_iter(vec![("hello".to_owned(), "world".to_owned())])));
let mut req = TestRequest::with_header( let mut req = TestRequest::with_header(
header::CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8") 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")); req.payload_mut().unread_data(Bytes::from_static(b"hello=world"));
let result = req.urlencoded().poll().ok().unwrap(); let result = req.urlencoded().poll().ok().unwrap();
assert_eq!(result, Async::Ready( assert_eq!(result, Async::Ready(Info{hello: "world".to_owned()}));
HashMap::from_iter(vec![("hello".to_owned(), "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::<Info>::from_request(&req).poll().unwrap() {
Async::Ready(s) => {
assert_eq!(s.hello, "world");
},
_ => unreachable!(),
}
} }
#[test] #[test]

View File

@ -56,7 +56,7 @@ use httpresponse::HttpResponse;
/// username: String, /// username: String,
/// } /// }
/// ///
/// /// extract `Info` using serde /// /// deserialize `Info` from request's body
/// fn index(info: Json<Info>) -> Result<String> { /// fn index(info: Json<Info>) -> Result<String> {
/// Ok(format!("Welcome {}!", info.username)) /// Ok(format!("Welcome {}!", info.username))
/// } /// }
@ -129,7 +129,6 @@ impl<T, S> FromRequest<S> for Json<T>
/// * content type is not `application/json` /// * content type is not `application/json`
/// * content length is greater than 256k /// * content length is greater than 256k
/// ///
///
/// # Server example /// # Server example
/// ///
/// ```rust /// ```rust

View File

@ -139,7 +139,7 @@ pub use body::{Body, Binary};
pub use json::Json; pub use json::Json;
pub use de::{Path, Query}; pub use de::{Path, Query};
pub use application::App; pub use application::App;
pub use httpmessage::HttpMessage; pub use httpmessage::{HttpMessage, Form};
pub use httprequest::HttpRequest; pub use httprequest::HttpRequest;
pub use httpresponse::HttpResponse; pub use httpresponse::HttpResponse;
pub use handler::{Either, Responder, AsyncResponder, FutureResponse, State}; pub use handler::{Either, Responder, AsyncResponder, FutureResponse, State};