Skip to content
This repository has been archived by the owner on Feb 10, 2019. It is now read-only.

Commit

Permalink
add body parsing for form posts and json
Browse files Browse the repository at this point in the history
  • Loading branch information
softprops committed Jun 6, 2018
1 parent 2269534 commit 1cd5e14
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 12 deletions.
35 changes: 34 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
# 0.1.0 (unreleased)
# 0.1.1 (unreleased)

* bug fix - support for reading host from "host" (lowercase) in addition to "Host"
* feature - add support for "application/x-www-form-urlencoded" and "application/json"
parsed request bodies with `lando::RequestExt#payload()`

```rust
#[macro_use] extern crate cpython;
#[macro_use] extern crate lando;
#[macro_use] extern crate serde_deserialize;

use lando::{Response, RequestEx};

#[derive(Deserialize, Debug)]
struct Params {
x: usize,
y: usize
}

gateway!(
|req, _| => Ok(
Response::new(
req.payload::<Params>().map(
|params| format!(
"the answer is {}", params.y + params.y
)
).unwrap_or_else(
"try again".to_string()
)
)
);
```

# 0.1.0

* initial release
145 changes: 138 additions & 7 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ use std::collections::HashMap;
use body::Body;
use request::{GatewayRequest, RequestContext};
use response::GatewayResponse;
use rust_http::header::CONTENT_TYPE;
use rust_http::{Request as HttpRequest, Response as HttpResponse};
use serde::Deserialize;
use serde_json;
use serde_urlencoded;

/// API gateway pre-parsed http query string parameters
struct QueryStringParameters(HashMap<String, String>);
Expand All @@ -20,7 +24,43 @@ struct PathParameters(HashMap<String, String>);
struct StageVariables(HashMap<String, String>);

/// Extentions for `lando::Request` objects that
/// provide access to API gateway features
/// provide access to [API gateway features](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format)
///
/// In addition, you can also access a request's body in deserialized format
/// for payloads sent in `application/x-www-form-urlencoded` or
/// `application/x-www-form-urlencoded` format
///
/// ```rust
/// #[macro_use] extern crate cpython;
/// #[macro_use] extern crate lando;
/// #[macro_use] extern crate serde_derive;
///
/// use lando::{Response, RequestExt};
///
/// #[derive(Debug,Deserialize,Default)]
/// struct Args {
/// #[serde(default)]
/// x: usize,
/// #[serde(default)]
/// y: usize
/// }
///
/// # fn main() {
/// gateway!(|request, _| {
/// let args: Args = request.payload().unwrap_or_default();
/// Ok(
/// Response::new(
/// format!(
/// "{} + {} = {}",
/// args.x,
/// args.y,
/// args.x + args.y
/// )
/// )
/// )
/// });
/// # }
/// ```
pub trait RequestExt {
/// Return pre-parsed http query string parameters
/// associated with the API gateway request
Expand All @@ -33,9 +73,19 @@ pub trait RequestExt {
fn stage_variables(&self) -> HashMap<String, String>;
/// Return request context assocaited with the API gateway request
fn request_context(&self) -> RequestContext;

/// Return the result of a payload parsed into a serde Deserializeable
/// type.
///
/// Currently only `application/x-www-form-urlencoded`
/// and `application/json` flavors of content type
/// are supported
fn payload<D>(&self) -> Option<D>
where
for<'de> D: Deserialize<'de>;
}

impl<T> RequestExt for HttpRequest<T> {
impl RequestExt for HttpRequest<super::Body> {
fn query_string_parameters(&self) -> HashMap<String, String> {
self.extensions()
.get::<QueryStringParameters>()
Expand All @@ -61,6 +111,21 @@ impl<T> RequestExt for HttpRequest<T> {
.map(|ext| ext.clone())
.unwrap_or(Default::default())
}

fn payload<D>(&self) -> Option<D>
where
for<'de> D: Deserialize<'de>,
{
self.headers()
.get(CONTENT_TYPE)
.and_then(|ct| match ct.to_str() {
Ok("application/x-www-form-urlencoded") => {
serde_urlencoded::from_bytes(self.body().as_ref()).ok()
}
Ok("application/json") => serde_json::from_slice(self.body().as_ref()).ok(),
_ => None,
})
}
}

// resolve a gateway reqponse for an http::Response
Expand Down Expand Up @@ -107,12 +172,15 @@ impl From<GatewayRequest> for HttpRequest<Body> {
request_context,
} = value;

// build an http::Result from a lando::Request
// build an http::Request from a lando::Request
let mut builder = HttpRequest::builder();
builder.method(http_method.as_str()).uri({
format!(
"https://{}{}",
headers.get("Host").unwrap_or(&String::new()),
headers
.get("Host")
.or_else(|| headers.get("host"))
.unwrap_or(&String::new()),
path
)
});
Expand Down Expand Up @@ -142,14 +210,15 @@ impl From<GatewayRequest> for HttpRequest<Body> {
#[cfg(test)]
mod tests {
use super::GatewayRequest;
use RequestExt;
use rust_http::header::{CONTENT_TYPE, HOST};
use rust_http::Request as HttpRequest;
use std::collections::HashMap;
use RequestExt;

#[test]
fn requests_convert() {
let mut headers = HashMap::new();
headers.insert("Host".to_owned(), "www.rust-lang.org".to_owned());
headers.insert(HOST.as_str().to_string(), "www.rust-lang.org".to_owned());
let gwr: GatewayRequest = GatewayRequest {
path: "/foo".into(),
http_method: "GET".into(),
Expand All @@ -167,7 +236,7 @@ mod tests {
#[test]
fn requests_have_query_string_ext() {
let mut headers = HashMap::new();
headers.insert("Host".to_owned(), "www.rust-lang.org".to_owned());
headers.insert(HOST.as_str().to_string(), "www.rust-lang.org".to_owned());
let mut query = HashMap::new();
query.insert("foo".to_owned(), "bar".to_owned());
let gwr: GatewayRequest = GatewayRequest {
Expand All @@ -180,4 +249,66 @@ mod tests {
let actual = HttpRequest::from(gwr);
assert_eq!(actual.query_string_parameters(), query.clone());
}

#[test]
fn requests_have_form_post_parseable_payloads() {
let mut headers = HashMap::new();
headers.insert(HOST.as_str().to_string(), "www.rust-lang.org".to_owned());
headers.insert(
CONTENT_TYPE.as_str().to_string(),
"application/x-www-form-urlencoded".to_owned(),
);
#[derive(Deserialize, PartialEq, Debug)]
struct Payload {
foo: String,
baz: usize,
}
let gwr: GatewayRequest = GatewayRequest {
path: "/foo".into(),
http_method: "GET".into(),
headers: headers,
body: Some("foo=bar&baz=2".into()),
..Default::default()
};
let actual = HttpRequest::from(gwr);
let payload: Option<Payload> = actual.payload();
assert_eq!(
payload,
Some(Payload {
foo: "bar".into(),
baz: 2
})
)
}

#[test]
fn requests_have_json_parseable_payloads() {
let mut headers = HashMap::new();
headers.insert(HOST.as_str().to_string(), "www.rust-lang.org".to_owned());
headers.insert(
CONTENT_TYPE.as_str().to_string(),
"application/json".to_owned(),
);
#[derive(Deserialize, PartialEq, Debug)]
struct Payload {
foo: String,
baz: usize,
}
let gwr: GatewayRequest = GatewayRequest {
path: "/foo".into(),
http_method: "GET".into(),
headers: headers,
body: Some(r#"{"foo":"bar", "baz": 2}"#.into()),
..Default::default()
};
let actual = HttpRequest::from(gwr);
let payload: Option<Payload> = actual.payload();
assert_eq!(
payload,
Some(Payload {
foo: "bar".into(),
baz: 2
})
)
}
}
12 changes: 8 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
//! by building in an environment similar to Lambda's, like [this Docker
//! container](https://hub.docker.com/r/softprops/lambda-rust/).
//!
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate base64;
extern crate bytes;
extern crate cpython;
Expand All @@ -88,6 +91,7 @@ extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate serde_urlencoded;

// Std
use std::error::Error as StdError;
Expand Down Expand Up @@ -179,9 +183,9 @@ where
///
/// You can also the provide `gateway!` macro with a named function
///
/// The request argument is just a regular http::Request type but you can
/// access API gateway features, like path and query string parameters, and
/// stage variables by importing [lando::RequestExt`](trait.RequestExt.html)
/// The request argument is just a regular `http::Request` type but you can
/// extend with API gateway features, like path and query string parameters, and
/// more by importing [lando::RequestExt`](trait.RequestExt.html)
///
/// The context argument is [same type](struct.LambdaContext.html) used within the crowbar crate
///
Expand Down Expand Up @@ -229,7 +233,7 @@ where
/// crate-type = ["cdylib"]
/// ```
///
/// You then also need to change the names of the library indentifiers, expected by
/// You then also need to change the names of the library identifiers, expected by
/// the [cpython crate](https://dgrunwald.github.io/rust-cpython/doc/cpython/macro.py_module_initializer.html),
/// by using the following `gateway!` format. This pattern may no longer needed
/// the std library's [concat_idents!](https://doc.rust-lang.org/std/macro.concat_idents.html)
Expand Down

0 comments on commit 1cd5e14

Please sign in to comment.