1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-28 01:32:57 +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
* 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`

View File

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

View File

@ -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<String, String>` 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<HttpResponse> {
/// req.urlencoded() // <- get UrlEncoded future
/// Box::new(
/// req.urlencoded::<HashMap<String, String>>() // <- get UrlEncoded future
/// .from_err()
/// .and_then(|params| { // <- url encoded parameters
/// println!("==== BODY ==== {:?}", params);
/// Ok(HttpResponse::Ok().into())
/// })
/// .responder()
/// }))
/// }
/// # fn main() {}
/// ```
fn urlencoded(self) -> UrlEncoded<Self>
fn urlencoded<T: DeserializeOwned>(self) -> UrlEncoded<Self, T>
where Self: Stream<Item=Bytes, Error=PayloadError> + Sized
{
UrlEncoded::new(self)
@ -321,14 +325,14 @@ impl<T> Future for MessageBody<T>
}
/// Future that resolves to a parsed urlencoded values.
pub struct UrlEncoded<T> {
pub struct UrlEncoded<T, U> {
req: Option<T>,
limit: usize,
fut: Option<Box<Future<Item=HashMap<String, String>, Error=UrlencodedError>>>,
fut: Option<Box<Future<Item=U, Error=UrlencodedError>>>,
}
impl<T> UrlEncoded<T> {
pub fn new(req: T) -> UrlEncoded<T> {
impl<T, U> UrlEncoded<T, U> {
pub fn new(req: T) -> UrlEncoded<T, U> {
UrlEncoded {
req: Some(req),
limit: 262_144,
@ -343,10 +347,11 @@ impl<T> UrlEncoded<T> {
}
}
impl<T> Future for UrlEncoded<T>
where T: HttpMessage + Stream<Item=Bytes, Error=PayloadError> + 'static
impl<T, U> Future for UrlEncoded<T, U>
where T: HttpMessage + Stream<Item=Bytes, Error=PayloadError> + 'static,
U: DeserializeOwned + 'static
{
type Item = HashMap<String, String>;
type Item = U;
type Error = UrlencodedError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
@ -385,13 +390,16 @@ impl<T> Future for UrlEncoded<T>
}
})
.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::<U>(&body)
.map_err(|_| UrlencodedError::Parse)
} 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));
}
@ -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)]
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::<Info>()
.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::<Info>()
.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::<Info>()
.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::<Info>()
.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::<Info>().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::<Info>::from_request(&req).poll().unwrap() {
Async::Ready(s) => {
assert_eq!(s.hello, "world");
},
_ => unreachable!(),
}
}
#[test]

View File

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

View File

@ -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};