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:
parent
280c8d87f8
commit
cbf4c61eb5
@ -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`
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
///
|
///
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
@ -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};
|
||||||
|
Loading…
Reference in New Issue
Block a user