mirror of
https://github.com/actix/actix-web.git
synced 2024-11-26 03:21:08 +00:00
add urlencoded body extractor
This commit is contained in:
parent
280c8d87f8
commit
cbf4c61eb5
6 changed files with 127 additions and 43 deletions
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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
|
||||
/// .from_err()
|
||||
/// .and_then(|params| { // <- url encoded parameters
|
||||
/// println!("==== BODY ==== {:?}", params);
|
||||
/// Ok(HttpResponse::Ok().into())
|
||||
/// })
|
||||
/// .responder()
|
||||
/// 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())
|
||||
/// }))
|
||||
/// }
|
||||
/// # 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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
|
|
Loading…
Reference in a new issue