Skip to content

feat(lambda-http): create separate trait for lambda events body #473

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lambda-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ serde_urlencoded = "0.7.0"
query_map = { version = "0.4", features = ["url-query"] }

[dev-dependencies]
hyper = "0.14"
log = "^0.4"
maplit = "1.0"
tokio = { version = "1.0", features = ["macros"] }
tower-http = { version = "0.2", features = ["cors"] }
tower-http = { version = "0.2", features = ["cors"] }
144 changes: 96 additions & 48 deletions lambda-http/src/ext.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Extension methods for `http::Request` types
//! Extension methods for `http::Request` and `crate::Request` types

use crate::{request::RequestContext, Body};
use crate::{request::RequestContext, Request};
use http_body::Body as HttpBody;
use lambda_runtime::Context;
use query_map::QueryMap;
use serde::{de::value::Error as SerdeError, Deserialize};
Expand Down Expand Up @@ -30,6 +31,7 @@ pub(crate) struct RawHttpPath(pub(crate) String);
pub enum PayloadError {
/// Returned when `application/json` bodies fail to deserialize a payload
Json(serde_json::Error),
UnsupportedFormat(Box<dyn std::error::Error + Send + Sync + 'static>),
/// Returned when `application/x-www-form-urlencoded` bodies fail to deserialize a payload
WwwFormUrlEncoded(SerdeError),
}
Expand All @@ -38,6 +40,7 @@ impl fmt::Display for PayloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PayloadError::Json(json) => writeln!(f, "failed to parse payload from application/json {}", json),
PayloadError::UnsupportedFormat(err) => writeln!(f, "unsupported payload {}", err),
PayloadError::WwwFormUrlEncoded(form) => writeln!(
f,
"failed to parse payload from application/x-www-form-urlencoded {}",
Expand All @@ -51,56 +54,39 @@ impl Error for PayloadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
PayloadError::Json(json) => Some(json),
PayloadError::UnsupportedFormat(err) => Some(err.as_ref()),
PayloadError::WwwFormUrlEncoded(form) => Some(form),
}
}
}

/// Extentions for `lambda_http::Request` structs that
/// Extentions for `http::Request` structs that
/// provide access to [API gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format)
/// and [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html)
/// features.
///
/// # Examples
///
/// A request's body can be deserialized if its correctly encoded as per
/// the request's `Content-Type` header. The two supported content types are
/// `application/x-www-form-urlencoded` and `application/json`.
///
/// The following handler will work an http request body of `x=1&y=2`
/// as well as `{"x":1, "y":2}` respectively.
/// We can retrieve the stage variables from API Gateway and return it to the user.
///
/// ```rust,no_run
/// use lambda_http::{service_fn, Error, Context, Body, IntoResponse, Request, Response, RequestExt};
/// use serde::Deserialize;
///
/// #[derive(Debug,Deserialize,Default)]
/// struct Args {
/// #[serde(default)]
/// x: usize,
/// #[serde(default)]
/// y: usize
/// }
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// lambda_http::run(service_fn(add)).await?;
/// lambda_http::run(service_fn(retrieve_stage_variables)).await?;
/// Ok(())
/// }
///
/// async fn add(
/// async fn retrieve_stage_variables(
/// request: Request
/// ) -> Result<Response<Body>, Error> {
/// let args: Args = request.payload()
/// .unwrap_or_else(|_parse_err| None)
/// .unwrap_or_default();
/// let stage_variables = request.stage_variables();
/// Ok(
/// Response::new(
/// format!(
/// "{} + {} = {}",
/// args.x,
/// args.y,
/// args.x + args.y
/// "The stage variables: {:?}",
/// stage_variables,
/// ).into()
/// )
/// )
Expand Down Expand Up @@ -165,27 +151,17 @@ pub trait RequestExt {
/// Return request context data assocaited with the ALB or 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
///
/// A [PayloadError](enum.PayloadError.html) will be returned for undeserializable
/// payloads. If no body is provided, `Ok(None)` will be returned.
fn payload<D>(&self) -> Result<Option<D>, PayloadError>
where
for<'de> D: Deserialize<'de>;

/// Return the Lambda function context associated with the request
fn lambda_context(&self) -> Context;

/// Configures instance with lambda context
fn with_lambda_context(self, context: Context) -> Self;
}

impl RequestExt for http::Request<Body> {
impl <B> RequestExt for http::Request<B>
where
B: HttpBody
{
fn raw_http_path(&self) -> String {
self.extensions()
.get::<RawHttpPath>()
Expand Down Expand Up @@ -267,11 +243,79 @@ impl RequestExt for http::Request<Body> {
s.extensions_mut().insert(context);
s
}
}

/// Extentions for `lambda_http::Request` structs containing specific Lambda events body that
/// provide access to [API gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format)
/// and [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html)
/// features.
///
/// # Examples
///
/// A request's body can be deserialized if its correctly encoded as per
/// the request's `Content-Type` header. The two supported content types are
/// `application/x-www-form-urlencoded` and `application/json`.
///
/// The following handler will work an http request body of `x=1&y=2`
/// as well as `{"x":1, "y":2}` respectively.
///
/// ```rust,no_run
/// use lambda_http::{service_fn, Error, Context, Body, IntoResponse, Request, Response, RequestExt, RequestExtBody};
/// use serde::Deserialize;
///
/// #[derive(Debug,Deserialize,Default)]
/// struct Args {
/// #[serde(default)]
/// x: usize,
/// #[serde(default)]
/// y: usize
/// }
///
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// lambda_http::run(service_fn(add)).await?;
/// Ok(())
/// }
///
/// async fn add(
/// request: Request
/// ) -> Result<Response<Body>, Error> {
/// let args: Args = request.payload()
/// .unwrap_or_else(|_parse_err| None)
/// .unwrap_or_default();
/// Ok(
/// Response::new(
/// format!(
/// "{} + {} = {}",
/// args.x,
/// args.y,
/// args.x + args.y
/// ).into()
/// )
/// )
/// }
/// ```
pub trait RequestExtBody {
/// 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
///
/// A [PayloadError](enum.PayloadError.html) will be returned for undeserializable
/// payloads. If no body is provided, `Ok(None)` will be returned.
fn payload<D>(&self) -> Result<Option<D>, PayloadError>
where
for<'de> D: Deserialize<'de>;
}

impl RequestExtBody for Request {
fn payload<D>(&self) -> Result<Option<D>, PayloadError>
where
for<'de> D: Deserialize<'de>,
{
println!("RequestExtWithPayload::payload");
self.headers()
.get(http::header::CONTENT_TYPE)
.map(|ct| match ct.to_str() {
Expand All @@ -296,7 +340,8 @@ impl RequestExt for http::Request<Body> {

#[cfg(test)]
mod tests {
use crate::{Body, Request, RequestExt};
use crate::{Body, Request, RequestExt, RequestExtBody};
use hyper::Body as HyperBody;
use serde::Deserialize;

#[test]
Expand Down Expand Up @@ -333,7 +378,7 @@ mod tests {
foo: String,
baz: usize,
}
let request = http::Request::builder()
let request: Request = http::Request::builder()
.header("Content-Type", "application/x-www-form-urlencoded")
.body(Body::from("foo=bar&baz=2"))
.expect("failed to build request");
Expand All @@ -354,7 +399,7 @@ mod tests {
foo: String,
baz: usize,
}
let request = http::Request::builder()
let request: Request = http::Request::builder()
.header("Content-Type", "application/json")
.body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
.expect("failed to build request");
Expand All @@ -375,7 +420,7 @@ mod tests {
foo: String,
baz: usize,
}
let request = http::Request::builder()
let request: Request = http::Request::builder()
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.body(Body::from("foo=bar&baz=2"))
.expect("failed to build request");
Expand All @@ -396,7 +441,7 @@ mod tests {
foo: String,
baz: usize,
}
let request = http::Request::builder()
let request: Request = http::Request::builder()
.header("Content-Type", "application/json; charset=UTF-8")
.body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
.expect("failed to build request");
Expand All @@ -417,7 +462,7 @@ mod tests {
foo: String,
baz: usize,
}
let request = http::Request::builder()
let request: Request = http::Request::builder()
.body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
.expect("failed to bulid request");
let payload: Option<Payload> = request.payload().unwrap_or_default();
Expand All @@ -426,7 +471,10 @@ mod tests {

#[test]
fn requests_can_mock_raw_http_path_ext() {
let request = Request::default().with_raw_http_path("/raw-path");
let request: http::Request<HyperBody> = http::Request::builder()
.body(HyperBody::from(r#"foo=bar"#))
.expect("failed to build request")
.with_raw_http_path("/raw-path");
assert_eq!("/raw-path", request.raw_http_path().as_str());
}
}
2 changes: 1 addition & 1 deletion lambda-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub use lambda_runtime::{self, service_fn, tower, Context, Error, Service};
pub mod ext;
pub mod request;
mod response;
pub use crate::{ext::RequestExt, response::IntoResponse};
pub use crate::{ext::{RequestExt, RequestExtBody}, response::IntoResponse};
use crate::{
request::{LambdaRequest, RequestOrigin},
response::LambdaResponse,
Expand Down