From 785cb7626e5ab5d4538e68fda99712667f476c3e Mon Sep 17 00:00:00 2001 From: Daniel Cormier Date: Mon, 20 Feb 2023 16:27:07 -0500 Subject: [PATCH] Lambda-related helper methods for `http::request::Parts` and `http::Extensions` --- examples/http-cors/src/main.rs | 19 +- examples/http-query-parameters/src/main.rs | 19 +- examples/http-shared-resource/src/main.rs | 35 +- lambda-http/README.md | 49 +- lambda-http/src/ext.rs | 517 --------------- lambda-http/src/ext/extensions.rs | 627 ++++++++++++++++++ lambda-http/src/ext/mod.rs | 7 + lambda-http/src/ext/request.rs | 220 ++++++ lambda-http/src/lib.rs | 17 +- lambda-http/src/request.rs | 70 +- lambda-http/src/streaming.rs | 4 +- lambda-integration-tests/src/bin/http-fn.rs | 2 +- .../src/bin/http-trait.rs | 2 +- lambda-runtime/src/types.rs | 2 +- 14 files changed, 985 insertions(+), 605 deletions(-) delete mode 100644 lambda-http/src/ext.rs create mode 100644 lambda-http/src/ext/extensions.rs create mode 100644 lambda-http/src/ext/mod.rs create mode 100644 lambda-http/src/ext/request.rs diff --git a/examples/http-cors/src/main.rs b/examples/http-cors/src/main.rs index f1ee0955..ea1f0372 100644 --- a/examples/http-cors/src/main.rs +++ b/examples/http-cors/src/main.rs @@ -32,11 +32,16 @@ async fn main() -> Result<(), Error> { } async fn func(event: Request) -> Result, Error> { - Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => format!("Hello, {}!", first_name).into_response().await, - _ => Response::builder() - .status(400) - .body("Empty first name".into()) - .expect("failed to render response"), - }) + Ok( + match event + .query_string_parameters_ref() + .and_then(|params| params.first("first_name")) + { + Some(first_name) => format!("Hello, {}!", first_name).into_response().await, + None => Response::builder() + .status(400) + .body("Empty first name".into()) + .expect("failed to render response"), + }, + ) } diff --git a/examples/http-query-parameters/src/main.rs b/examples/http-query-parameters/src/main.rs index 2a33829c..0300df37 100644 --- a/examples/http-query-parameters/src/main.rs +++ b/examples/http-query-parameters/src/main.rs @@ -6,13 +6,18 @@ use lambda_http::{run, service_fn, Error, IntoResponse, Request, RequestExt, Res /// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples async fn function_handler(event: Request) -> Result { // Extract some useful information from the request - Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => format!("Hello, {}!", first_name).into_response().await, - _ => Response::builder() - .status(400) - .body("Empty first name".into()) - .expect("failed to render response"), - }) + Ok( + match event + .query_string_parameters_ref() + .and_then(|params| params.first("first_name")) + { + Some(first_name) => format!("Hello, {}!", first_name).into_response().await, + None => Response::builder() + .status(400) + .body("Empty first name".into()) + .expect("failed to render response"), + }, + ) } #[tokio::main] diff --git a/examples/http-shared-resource/src/main.rs b/examples/http-shared-resource/src/main.rs index e8d65e80..16493452 100644 --- a/examples/http-shared-resource/src/main.rs +++ b/examples/http-shared-resource/src/main.rs @@ -32,18 +32,29 @@ async fn main() -> Result<(), Error> { // Define a closure here that makes use of the shared client. let handler_func_closure = move |event: Request| async move { - Result::, Error>::Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => { - shared_client_ref - .response(event.lambda_context().request_id, first_name) - .into_response() - .await - } - _ => Response::builder() - .status(400) - .body("Empty first name".into()) - .expect("failed to render response"), - }) + Result::, Error>::Ok( + match event + .query_string_parameters_ref() + .and_then(|params| params.first("first_name")) + { + Some(first_name) => { + shared_client_ref + .response( + event + .lambda_context_ref() + .map(|ctx| ctx.request_id.clone()) + .unwrap_or_default(), + first_name, + ) + .into_response() + .await + } + None => Response::builder() + .status(400) + .body("Empty first name".into()) + .expect("failed to render response"), + }, + ) }; // Pass the closure to the runtime here. diff --git a/lambda-http/README.md b/lambda-http/README.md index 1eef90c6..161a5576 100644 --- a/lambda-http/README.md +++ b/lambda-http/README.md @@ -6,8 +6,8 @@ lambda-http handler is made of: -* Request - Represents an HTTP request -* IntoResponse - Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] +* `Request` - Represents an HTTP request +* `IntoResponse` - Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] We are able to handle requests from: @@ -15,32 +15,35 @@ We are able to handle requests from: * AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) * AWS [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) -Thanks to the Request type we can seemsly handle proxy integrations without the worry to specify the specific service type. +Thanks to the `Request` type we can seamlessly handle proxy integrations without the worry to specify the specific service type. -There is also an Extentions for `lambda_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. +There is also an extension for `lambda_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. For example some handy extensions: -* query_string_parameters - Return pre-parsed http query string parameters, parameters provided after the `?` portion of a url associated with the request -* path_parameters - Return pre-extracted path parameters, parameter provided in url placeholders `/foo/{bar}/baz/{boom}` associated with the request -* payload - Return the Result of a payload parsed into a serde Deserializeable type +* `query_string_parameters` - Return pre-parsed http query string parameters, parameters provided after the `?` portion of a url associated with the request +* `path_parameters` - Return pre-extracted path parameters, parameter provided in url placeholders `/foo/{bar}/baz/{qux}` associated with the request +* `lambda_context` - Return the Lambda context for the invocation; see the [runtime docs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next) +* `request_context` - Return the ALB/API Gateway request context +* payload - Return the Result of a payload parsed into a type that implements `serde::Deserialize` + +See the `lambda_http::RequestPayloadExt` and `lambda_http::RequestExt` traits for more extensions. ## Examples -Here you will find a few examples to handle basic scenarions: +Here you will find a few examples to handle basic scenarios: -* Reading a JSON from a body and deserialise into a structure -* Reading querystring parameters +* Reading a JSON from a body and deserialize into a structure +* Reading query string parameters * Lambda Request Authorizer -* Passing the Lambda execution context initialisation to the handler +* Passing the Lambda execution context initialization to the handler -### Reading a JSON from a body and deserialise into a structure +### Reading a JSON from a body and deserialize into a structure The code below creates a simple API Gateway proxy (HTTP, REST) that accept in input a JSON payload. ```rust -use http::Response; -use lambda_http::{run, http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; +use lambda_http::{run, http::{StatusCode, Response}, service_fn, Error, IntoResponse, Request, RequestPayloadExt}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -77,11 +80,10 @@ pub struct MyPayload { } ``` -### Reading querystring parameters +### Reading query string parameters ```rust -use http::Response; -use lambda_http::{run, http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; +use lambda_http::{run, http::{StatusCode, Response}, service_fn, Error, RequestExt, IntoResponse, Request}; use serde_json::json; #[tokio::main] @@ -96,8 +98,8 @@ async fn main() -> Result<(), Error> { } pub async fn function_handler(event: Request) -> Result { - let name = event.query_string_parameters() - .first("name") + let name = event.query_string_parameters_ref() + .and_then(|params| params.first("name")) .unwrap_or_else(|| "stranger") .to_string(); @@ -176,15 +178,14 @@ pub fn custom_authorizer_response(effect: &str, principal: &str, method_arn: &st } ``` -## Passing the Lambda execution context initialisation to the handler +## Passing the Lambda execution context initialization to the handler One of the [best practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html) is to take advantage of execution environment reuse to improve the performance of your function. Initialize SDK clients and database connections outside the function handler. Subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time. ```rust use aws_sdk_dynamodb::model::AttributeValue; use chrono::Utc; -use http::Response; -use lambda_http::{run, http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; +use lambda_http::{run, http::{StatusCode, Response}, service_fn, Error, RequestExt, IntoResponse, Request}; use serde_json::json; #[tokio::main] @@ -207,8 +208,8 @@ async fn main() -> Result<(), Error> { pub async fn function_handler(dynamodb_client: &aws_sdk_dynamodb::Client, event: Request) -> Result { let table = std::env::var("TABLE_NAME").expect("TABLE_NAME must be set"); - let name = event.query_string_parameters() - .first("name") + let name = event.query_string_parameters_ref() + .and_then(|params| params.first("name")) .unwrap_or_else(|| "stranger") .to_string(); diff --git a/lambda-http/src/ext.rs b/lambda-http/src/ext.rs deleted file mode 100644 index f034c686..00000000 --- a/lambda-http/src/ext.rs +++ /dev/null @@ -1,517 +0,0 @@ -//! Extension methods for `http::Request` types - -use crate::{request::RequestContext, Body}; -use aws_lambda_events::query_map::QueryMap; -use lambda_runtime::Context; -use serde::{de::value::Error as SerdeError, Deserialize}; -use std::{error::Error, fmt}; - -/// ALB/API gateway pre-parsed http query string parameters -pub(crate) struct QueryStringParameters(pub(crate) QueryMap); - -/// API gateway pre-extracted url path parameters -/// -/// These will always be empty for ALB requests -pub(crate) struct PathParameters(pub(crate) QueryMap); - -/// API gateway configured -/// [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) -/// -/// These will always be empty for ALB requests -pub(crate) struct StageVariables(pub(crate) QueryMap); - -/// ALB/API gateway raw http path without any stage information -pub(crate) struct RawHttpPath(pub(crate) String); - -/// Request payload deserialization errors -/// -/// Returned by [`RequestExt#payload()`](trait.RequestExt.html#tymethod.payload) -#[derive(Debug)] -pub enum PayloadError { - /// Returned when `application/json` bodies fail to deserialize a payload - Json(serde_json::Error), - /// Returned when `application/x-www-form-urlencoded` bodies fail to deserialize a payload - WwwFormUrlEncoded(SerdeError), -} - -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::WwwFormUrlEncoded(form) => writeln!( - f, - "failed to parse payload from application/x-www-form-urlencoded {form}" - ), - } - } -} - -impl Error for PayloadError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - PayloadError::Json(json) => Some(json), - PayloadError::WwwFormUrlEncoded(form) => Some(form), - } - } -} - -/// Extensions for `lambda_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. -/// -/// ```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?; -/// Ok(()) -/// } -/// -/// async fn add( -/// request: Request -/// ) -> Result, 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 RequestExt { - /// Return the raw http path for a request without any stage information. - fn raw_http_path(&self) -> String; - - /// Return the raw http path for a request without any stage information. - fn raw_http_path_str(&self) -> &str; - - /// Configures instance with the raw http path. - fn with_raw_http_path(self, path: &str) -> Self; - - /// Return pre-parsed http query string parameters, parameters - /// provided after the `?` portion of a url, - /// associated with the API gateway request. - /// - /// The yielded value represents both single and multi-valued - /// parameters alike. When multiple query string parameters with the same - /// name are expected, `query_string_parameters().all("many")` to retrieve them all. - /// - /// No query parameters will yield an empty `QueryMap`. - fn query_string_parameters(&self) -> QueryMap; - - /// Return pre-parsed http query string parameters, parameters - /// provided after the `?` portion of a url, - /// associated with the API gateway request. - /// - /// The yielded value represents both single and multi-valued - /// parameters alike. When multiple query string parameters with the same - /// name are expected, - /// `query_string_parameters_ref().and_then(|params| params.all("many"))` to - /// retrieve them all. - /// - /// No query parameters will yield `None`. - fn query_string_parameters_ref(&self) -> Option<&QueryMap>; - - /// Configures instance with query string parameters - /// - /// This is intended for use in mock testing contexts. - fn with_query_string_parameters(self, parameters: Q) -> Self - where - Q: Into; - - /// Return pre-extracted path parameters, parameter provided in url placeholders - /// `/foo/{bar}/baz/{boom}`, - /// associated with the API gateway request. No path parameters - /// will yield an empty `QueryMap` - /// - /// These will always be empty for ALB triggered requests - fn path_parameters(&self) -> QueryMap; - - /// Return pre-extracted path parameters, parameter provided in url placeholders - /// `/foo/{bar}/baz/{boom}`, - /// associated with the API gateway request. No path parameters - /// will yield `None` - /// - /// These will always be `None` for ALB triggered requests - fn path_parameters_ref(&self) -> Option<&QueryMap>; - - /// Configures instance with path parameters - /// - /// This is intended for use in mock testing contexts. - fn with_path_parameters

(self, parameters: P) -> Self - where - P: Into; - - /// Return [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) - /// associated with the API gateway request. No stage parameters - /// will yield an empty `QueryMap` - /// - /// These will always be empty for ALB triggered requests - fn stage_variables(&self) -> QueryMap; - - /// Return [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) - /// associated with the API gateway request. No stage parameters - /// will yield `None` - /// - /// These will always be `None` for ALB triggered requests - fn stage_variables_ref(&self) -> Option<&QueryMap>; - - /// Configures instance with stage variables under #[cfg(test)] configurations - /// - /// This is intended for use in mock testing contexts. - #[cfg(test)] - fn with_stage_variables(self, variables: V) -> Self - where - V: Into; - - /// Return request context data associated with the ALB or API gateway request - fn request_context(&self) -> RequestContext; - - /// Return a reference to the request context data associated with the ALB or - /// API gateway request - fn request_context_ref(&self) -> Option<&RequestContext>; - - /// Configures instance with request context - /// - /// This is intended for use in mock testing contexts. - fn with_request_context(self, context: RequestContext) -> Self; - - /// 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(&self) -> Result, PayloadError> - where - for<'de> D: Deserialize<'de>; - - /// Return the Lambda function context associated with the request - fn lambda_context(&self) -> Context; - - /// Return a reference to the the Lambda function context associated with the - /// request - fn lambda_context_ref(&self) -> Option<&Context>; - - /// Configures instance with lambda context - fn with_lambda_context(self, context: Context) -> Self; -} - -impl RequestExt for http::Request { - fn raw_http_path(&self) -> String { - self.raw_http_path_str().to_string() - } - - fn raw_http_path_str(&self) -> &str { - self.extensions() - .get::() - .map(|RawHttpPath(path)| path.as_str()) - .unwrap_or_default() - } - - fn with_raw_http_path(self, path: &str) -> Self { - let mut s = self; - s.extensions_mut().insert(RawHttpPath(path.into())); - s - } - - fn query_string_parameters(&self) -> QueryMap { - self.query_string_parameters_ref().cloned().unwrap_or_default() - } - - fn query_string_parameters_ref(&self) -> Option<&QueryMap> { - self.extensions().get::().and_then( - |QueryStringParameters(params)| { - if params.is_empty() { - None - } else { - Some(params) - } - }, - ) - } - - fn with_query_string_parameters(self, parameters: Q) -> Self - where - Q: Into, - { - let mut s = self; - s.extensions_mut().insert(QueryStringParameters(parameters.into())); - s - } - - fn path_parameters(&self) -> QueryMap { - self.path_parameters_ref().cloned().unwrap_or_default() - } - - fn path_parameters_ref(&self) -> Option<&QueryMap> { - self.extensions().get::().and_then( - |PathParameters(params)| { - if params.is_empty() { - None - } else { - Some(params) - } - }, - ) - } - - fn with_path_parameters

(self, parameters: P) -> Self - where - P: Into, - { - let mut s = self; - s.extensions_mut().insert(PathParameters(parameters.into())); - s - } - - fn stage_variables(&self) -> QueryMap { - self.stage_variables_ref().cloned().unwrap_or_default() - } - - fn stage_variables_ref(&self) -> Option<&QueryMap> { - self.extensions().get::().and_then( - |StageVariables(vars)| { - if vars.is_empty() { - None - } else { - Some(vars) - } - }, - ) - } - - #[cfg(test)] - fn with_stage_variables(self, variables: V) -> Self - where - V: Into, - { - let mut s = self; - s.extensions_mut().insert(StageVariables(variables.into())); - s - } - - fn request_context(&self) -> RequestContext { - self.request_context_ref() - .cloned() - .expect("Request did not contain a request context") - } - - fn request_context_ref(&self) -> Option<&RequestContext> { - self.extensions().get::() - } - - fn with_request_context(self, context: RequestContext) -> Self { - let mut s = self; - s.extensions_mut().insert(context); - s - } - - fn lambda_context(&self) -> Context { - self.lambda_context_ref() - .cloned() - .expect("Request did not contain a lambda context") - } - - fn lambda_context_ref(&self) -> Option<&Context> { - self.extensions().get::() - } - - fn with_lambda_context(self, context: Context) -> Self { - let mut s = self; - s.extensions_mut().insert(context); - s - } - - fn payload(&self) -> Result, PayloadError> - where - for<'de> D: Deserialize<'de>, - { - self.headers() - .get(http::header::CONTENT_TYPE) - .map(|ct| match ct.to_str() { - Ok(content_type) => { - if content_type.starts_with("application/x-www-form-urlencoded") { - return serde_urlencoded::from_bytes::(self.body().as_ref()) - .map_err(PayloadError::WwwFormUrlEncoded) - .map(Some); - } else if content_type.starts_with("application/json") { - return serde_json::from_slice::(self.body().as_ref()) - .map_err(PayloadError::Json) - .map(Some); - } - - Ok(None) - } - _ => Ok(None), - }) - .unwrap_or_else(|| Ok(None)) - } -} - -#[cfg(test)] -mod tests { - use crate::{Body, Request, RequestExt}; - use serde::Deserialize; - - #[test] - fn requests_can_mock_query_string_parameters_ext() { - let mocked = hashmap! { - "foo".into() => vec!["bar".into()] - }; - let request = Request::default().with_query_string_parameters(mocked.clone()); - assert_eq!(request.query_string_parameters(), mocked.into()); - } - - #[test] - fn requests_can_mock_path_parameters_ext() { - let mocked = hashmap! { - "foo".into() => vec!["bar".into()] - }; - let request = Request::default().with_path_parameters(mocked.clone()); - assert_eq!(request.path_parameters(), mocked.into()); - } - - #[test] - fn requests_can_mock_stage_variables_ext() { - let mocked = hashmap! { - "foo".into() => vec!["bar".into()] - }; - let request = Request::default().with_stage_variables(mocked.clone()); - assert_eq!(request.stage_variables(), mocked.into()); - } - - #[test] - fn requests_have_form_post_parsable_payloads() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .header("Content-Type", "application/x-www-form-urlencoded") - .body(Body::from("foo=bar&baz=2")) - .expect("failed to build request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_have_json_parsable_payloads() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .header("Content-Type", "application/json") - .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) - .expect("failed to build request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_match_form_post_content_type_with_charset() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let 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"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_match_json_content_type_with_charset() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let 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"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_omitting_content_types_do_not_support_parsable_payloads() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) - .expect("failed to build request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!(payload, None); - } - - #[test] - fn requests_can_mock_raw_http_path_ext() { - let request = Request::default().with_raw_http_path("/raw-path"); - assert_eq!("/raw-path", request.raw_http_path().as_str()); - } -} diff --git a/lambda-http/src/ext/extensions.rs b/lambda-http/src/ext/extensions.rs new file mode 100644 index 00000000..e002d0ea --- /dev/null +++ b/lambda-http/src/ext/extensions.rs @@ -0,0 +1,627 @@ +//! Extension methods for `http::Extensions` and `http::Request` types + +use aws_lambda_events::query_map::QueryMap; +use http::request::Parts; +use lambda_runtime::Context; + +use crate::request::RequestContext; + +/// ALB/API gateway pre-parsed http query string parameters +pub(crate) struct QueryStringParameters(pub(crate) QueryMap); + +/// API gateway pre-extracted url path parameters +/// +/// These will always be empty for ALB requests +pub(crate) struct PathParameters(pub(crate) QueryMap); + +/// API gateway configured +/// [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) +/// +/// These will always be empty for ALB requests +pub(crate) struct StageVariables(pub(crate) QueryMap); + +/// ALB/API gateway raw http path without any stage information +pub(crate) struct RawHttpPath(pub(crate) String); + +/// Extensions for [`lambda_http::Request`], `http::request::Parts`, and `http::Extensions` 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. +/// +/// [`lambda_http::Request`]: crate::Request +pub trait RequestExt { + /// Return the raw http path for a request without any stage information. + fn raw_http_path(&self) -> &str; + + /// Configures instance with the raw http path. + fn with_raw_http_path(self, path: S) -> Self + where + S: Into; + + /// Return pre-parsed HTTP query string parameters, parameters + /// provided after the `?` portion of a URL, + /// associated with the API gateway request. + /// + /// The yielded value represents both single and multi-valued + /// parameters alike. When multiple query string parameters with the same + /// name are expected, use `query_string_parameters().all("many")` to + /// retrieve them all. + /// + /// Having no query parameters will yield an empty `QueryMap`. + fn query_string_parameters(&self) -> QueryMap; + + /// Return pre-parsed HTTP query string parameters, parameters + /// provided after the `?` portion of a URL, + /// associated with the API gateway request. + /// + /// The yielded value represents both single and multi-valued + /// parameters alike. When multiple query string parameters with the same + /// name are expected, use + /// `query_string_parameters_ref().and_then(|params| params.all("many"))` to + /// retrieve them all. + /// + /// Having no query parameters will yield `None`. + fn query_string_parameters_ref(&self) -> Option<&QueryMap>; + + /// Configures instance with query string parameters + /// + /// This is intended for use in mock testing contexts. + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into; + + /// Return pre-extracted path parameters, parameter provided in URL placeholders + /// `/foo/{bar}/baz/{qux}`, + /// associated with the API gateway request. Having no path parameters + /// will yield an empty `QueryMap`. + /// + /// These will always be empty for ALB triggered requests. + fn path_parameters(&self) -> QueryMap; + + /// Return pre-extracted path parameters, parameter provided in URL placeholders + /// `/foo/{bar}/baz/{qux}`, + /// associated with the API gateway request. Having no path parameters + /// will yield `None`. + /// + /// These will always be `None` for ALB triggered requests. + fn path_parameters_ref(&self) -> Option<&QueryMap>; + + /// Configures instance with path parameters + /// + /// This is intended for use in mock testing contexts. + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into; + + /// Return [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) + /// associated with the API gateway request. Having no stage parameters + /// will yield an empty `QueryMap`. + /// + /// These will always be empty for ALB triggered requests. + fn stage_variables(&self) -> QueryMap; + + /// Return [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) + /// associated with the API gateway request. Having no stage parameters + /// will yield `None`. + /// + /// These will always be `None` for ALB triggered requests. + fn stage_variables_ref(&self) -> Option<&QueryMap>; + + /// Configures instance with stage variables under `#[cfg(test)]` configurations + /// + /// This is intended for use in mock testing contexts. + #[cfg(test)] + fn with_stage_variables(self, variables: V) -> Self + where + V: Into; + + /// Return request context data associated with the ALB or + /// API gateway request + fn request_context(&self) -> RequestContext; + + /// Return a reference to the request context data associated with the ALB or + /// API gateway request + fn request_context_ref(&self) -> Option<&RequestContext>; + + /// Configures instance with request context + /// + /// This is intended for use in mock testing contexts. + fn with_request_context(self, context: RequestContext) -> Self; + + /// Return Lambda function context data associated with the + /// request + fn lambda_context(&self) -> Context; + + /// Return a reference to the Lambda function context data associated with the + /// request + fn lambda_context_ref(&self) -> Option<&Context>; + + /// Configures instance with lambda context + fn with_lambda_context(self, context: Context) -> Self; +} + +impl RequestExt for http::Extensions { + fn raw_http_path(&self) -> &str { + self.get::() + .map(|RawHttpPath(path)| path.as_str()) + .unwrap_or_default() + } + + fn with_raw_http_path(self, path: S) -> Self + where + S: Into, + { + let mut s = self; + s.insert(RawHttpPath(path.into())); + s + } + + fn query_string_parameters(&self) -> QueryMap { + self.query_string_parameters_ref().cloned().unwrap_or_default() + } + + fn query_string_parameters_ref(&self) -> Option<&QueryMap> { + self.get::().and_then( + |QueryStringParameters(params)| { + if params.is_empty() { + None + } else { + Some(params) + } + }, + ) + } + + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into, + { + let mut s = self; + s.insert(QueryStringParameters(parameters.into())); + s + } + + fn path_parameters(&self) -> QueryMap { + self.path_parameters_ref().cloned().unwrap_or_default() + } + + fn path_parameters_ref(&self) -> Option<&QueryMap> { + self.get::().and_then( + |PathParameters(params)| { + if params.is_empty() { + None + } else { + Some(params) + } + }, + ) + } + + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into, + { + let mut s = self; + s.insert(PathParameters(parameters.into())); + s + } + + fn stage_variables(&self) -> QueryMap { + self.stage_variables_ref().cloned().unwrap_or_default() + } + + fn stage_variables_ref(&self) -> Option<&QueryMap> { + self.get::() + .and_then(|StageVariables(vars)| if vars.is_empty() { None } else { Some(vars) }) + } + + #[cfg(test)] + fn with_stage_variables(self, variables: V) -> Self + where + V: Into, + { + let mut s = self; + s.insert(StageVariables(variables.into())); + s + } + + fn request_context(&self) -> RequestContext { + self.request_context_ref() + .cloned() + .expect("Request did not contain a request context") + } + + fn request_context_ref(&self) -> Option<&RequestContext> { + self.get::() + } + + fn with_request_context(self, context: RequestContext) -> Self { + let mut s = self; + s.insert(context); + s + } + + fn lambda_context(&self) -> Context { + self.lambda_context_ref() + .cloned() + .expect("Request did not contain a lambda context") + } + + fn lambda_context_ref(&self) -> Option<&Context> { + self.get::() + } + + fn with_lambda_context(self, context: Context) -> Self { + let mut s = self; + s.insert(context); + s + } +} + +impl RequestExt for Parts { + fn raw_http_path(&self) -> &str { + self.extensions.raw_http_path() + } + + fn with_raw_http_path(self, path: S) -> Self + where + S: Into, + { + let mut s = self; + s.extensions = s.extensions.with_raw_http_path(path); + + s + } + + fn query_string_parameters(&self) -> QueryMap { + self.extensions.query_string_parameters() + } + + fn query_string_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions.query_string_parameters_ref() + } + + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into, + { + let mut s = self; + s.extensions = s.extensions.with_query_string_parameters(parameters); + + s + } + + fn path_parameters(&self) -> QueryMap { + self.extensions.path_parameters() + } + + fn path_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions.path_parameters_ref() + } + + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into, + { + let mut s = self; + s.extensions = s.extensions.with_path_parameters(parameters); + + s + } + + fn stage_variables(&self) -> QueryMap { + self.extensions.stage_variables() + } + + fn stage_variables_ref(&self) -> Option<&QueryMap> { + self.extensions.stage_variables_ref() + } + + #[cfg(test)] + fn with_stage_variables(self, variables: V) -> Self + where + V: Into, + { + let mut s = self; + s.extensions = s.extensions.with_stage_variables(variables); + + s + } + + fn request_context(&self) -> RequestContext { + self.extensions.request_context() + } + + fn request_context_ref(&self) -> Option<&RequestContext> { + self.extensions.request_context_ref() + } + + fn with_request_context(self, context: RequestContext) -> Self { + let mut s = self; + s.extensions = s.extensions.with_request_context(context); + + s + } + + fn lambda_context(&self) -> Context { + self.extensions.lambda_context() + } + + fn lambda_context_ref(&self) -> Option<&Context> { + self.extensions.lambda_context_ref() + } + + fn with_lambda_context(self, context: Context) -> Self { + let mut s = self; + s.extensions = s.extensions.with_lambda_context(context); + + s + } +} + +fn map_req_ext(req: http::Request, f: F) -> http::Request +where + F: FnOnce(http::Extensions) -> http::Extensions, +{ + let (mut parts, body) = req.into_parts(); + parts.extensions = (f)(parts.extensions); + + http::Request::from_parts(parts, body) +} + +impl RequestExt for http::Request { + fn raw_http_path(&self) -> &str { + self.extensions().raw_http_path() + } + + fn with_raw_http_path(self, path: S) -> Self + where + S: Into, + { + map_req_ext(self, |ext| ext.with_raw_http_path(path)) + } + + fn query_string_parameters(&self) -> QueryMap { + self.extensions().query_string_parameters() + } + + fn query_string_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions().query_string_parameters_ref() + } + + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into, + { + map_req_ext(self, |ext| ext.with_query_string_parameters(parameters)) + } + + fn path_parameters(&self) -> QueryMap { + self.extensions().path_parameters() + } + + fn path_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions().path_parameters_ref() + } + + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into, + { + map_req_ext(self, |ext| ext.with_path_parameters(parameters)) + } + + fn stage_variables(&self) -> QueryMap { + self.extensions().stage_variables() + } + + fn stage_variables_ref(&self) -> Option<&QueryMap> { + self.extensions().stage_variables_ref() + } + + #[cfg(test)] + fn with_stage_variables(self, variables: V) -> Self + where + V: Into, + { + map_req_ext(self, |ext| ext.with_stage_variables(variables)) + } + + fn request_context(&self) -> RequestContext { + self.extensions().request_context() + } + + fn request_context_ref(&self) -> Option<&RequestContext> { + self.extensions().request_context_ref() + } + + fn with_request_context(self, context: RequestContext) -> Self { + map_req_ext(self, |ext| ext.with_request_context(context)) + } + + fn lambda_context(&self) -> Context { + self.extensions().lambda_context() + } + + fn lambda_context_ref(&self) -> Option<&Context> { + self.extensions().lambda_context_ref() + } + + fn with_lambda_context(self, context: Context) -> Self { + map_req_ext(self, |ext| ext.with_lambda_context(context)) + } +} + +#[cfg(test)] +mod tests { + use aws_lambda_events::query_map::QueryMap; + use http::Extensions; + + use crate::Request; + + use super::RequestExt; + + #[test] + fn extensions_can_mock_query_string_parameters_ext() { + let ext = Extensions::default(); + assert_eq!(ext.query_string_parameters_ref(), None); + assert_eq!(ext.query_string_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let ext = ext.with_query_string_parameters(mocked.clone()); + assert_eq!(ext.query_string_parameters_ref(), Some(&mocked)); + assert_eq!(ext.query_string_parameters(), mocked); + } + + #[test] + fn parts_can_mock_query_string_parameters_ext() { + let (parts, _) = Request::default().into_parts(); + assert_eq!(parts.query_string_parameters_ref(), None); + assert_eq!(parts.query_string_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let parts = parts.with_query_string_parameters(mocked.clone()); + assert_eq!(parts.query_string_parameters_ref(), Some(&mocked)); + assert_eq!(parts.query_string_parameters(), mocked); + } + + #[test] + fn requests_can_mock_query_string_parameters_ext() { + let request = Request::default(); + assert_eq!(request.query_string_parameters_ref(), None); + assert_eq!(request.query_string_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let request = request.with_query_string_parameters(mocked.clone()); + assert_eq!(request.query_string_parameters_ref(), Some(&mocked)); + assert_eq!(request.query_string_parameters(), mocked); + } + + #[test] + fn extensions_can_mock_path_parameters_ext() { + let ext = Extensions::default(); + assert_eq!(ext.path_parameters_ref(), None); + assert_eq!(ext.path_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let ext = ext.with_path_parameters(mocked.clone()); + assert_eq!(ext.path_parameters_ref(), Some(&mocked)); + assert_eq!(ext.path_parameters(), mocked); + } + + #[test] + fn parts_can_mock_path_parameters_ext() { + let (parts, _) = Request::default().into_parts(); + assert_eq!(parts.path_parameters_ref(), None); + assert_eq!(parts.path_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let parts = parts.with_path_parameters(mocked.clone()); + assert_eq!(parts.path_parameters_ref(), Some(&mocked)); + assert_eq!(parts.path_parameters(), mocked); + } + + #[test] + fn requests_can_mock_path_parameters_ext() { + let request = Request::default(); + assert_eq!(request.path_parameters_ref(), None); + assert_eq!(request.path_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let request = request.with_path_parameters(mocked.clone()); + assert_eq!(request.path_parameters_ref(), Some(&mocked)); + assert_eq!(request.path_parameters(), mocked); + } + + #[test] + fn extensions_can_mock_stage_variables_ext() { + let ext = Extensions::default(); + assert_eq!(ext.stage_variables_ref(), None); + assert_eq!(ext.stage_variables(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let ext = ext.with_stage_variables(mocked.clone()); + assert_eq!(ext.stage_variables_ref(), Some(&mocked)); + assert_eq!(ext.stage_variables(), mocked); + } + + #[test] + fn parts_can_mock_stage_variables_ext() { + let (parts, _) = Request::default().into_parts(); + assert_eq!(parts.stage_variables_ref(), None); + assert_eq!(parts.stage_variables(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let parts = parts.with_stage_variables(mocked.clone()); + assert_eq!(parts.stage_variables_ref(), Some(&mocked)); + assert_eq!(parts.stage_variables(), mocked); + } + + #[test] + fn requests_can_mock_stage_variables_ext() { + let request = Request::default(); + assert_eq!(request.stage_variables_ref(), None); + assert_eq!(request.stage_variables(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let request = request.with_stage_variables(mocked.clone()); + assert_eq!(request.stage_variables_ref(), Some(&mocked)); + assert_eq!(request.stage_variables(), mocked); + } + + #[test] + fn extensions_can_mock_raw_http_path_ext() { + let ext = Extensions::default().with_raw_http_path("/raw-path"); + assert_eq!("/raw-path", ext.raw_http_path()); + } + + #[test] + fn parts_can_mock_raw_http_path_ext() { + let (parts, _) = Request::default().into_parts(); + let parts = parts.with_raw_http_path("/raw-path"); + assert_eq!("/raw-path", parts.raw_http_path()); + } + + #[test] + fn requests_can_mock_raw_http_path_ext() { + let request = Request::default().with_raw_http_path("/raw-path"); + assert_eq!("/raw-path", request.raw_http_path()); + } +} diff --git a/lambda-http/src/ext/mod.rs b/lambda-http/src/ext/mod.rs new file mode 100644 index 00000000..81c64daa --- /dev/null +++ b/lambda-http/src/ext/mod.rs @@ -0,0 +1,7 @@ +//! Extension methods for `Request` types + +pub mod extensions; +pub mod request; + +pub use extensions::RequestExt; +pub use request::{PayloadError, RequestPayloadExt}; diff --git a/lambda-http/src/ext/request.rs b/lambda-http/src/ext/request.rs new file mode 100644 index 00000000..11c49012 --- /dev/null +++ b/lambda-http/src/ext/request.rs @@ -0,0 +1,220 @@ +//! Extension methods for `Request` types + +use std::{error::Error, fmt}; + +use serde::{ + de::{value::Error as SerdeError, DeserializeOwned}, + Deserialize, +}; + +use crate::Body; + +/// Request payload deserialization errors +/// +/// Returned by [`RequestPayloadExt::payload()`] +#[derive(Debug)] +pub enum PayloadError { + /// Returned when `application/json` bodies fail to deserialize a payload + Json(serde_json::Error), + /// Returned when `application/x-www-form-urlencoded` bodies fail to deserialize a payload + WwwFormUrlEncoded(SerdeError), +} + +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::WwwFormUrlEncoded(form) => writeln!( + f, + "failed to parse payload from application/x-www-form-urlencoded {form}" + ), + } + } +} + +impl Error for PayloadError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + PayloadError::Json(json) => Some(json), + PayloadError::WwwFormUrlEncoded(form) => Some(form), + } + } +} + +/// Extensions for `lambda_http::Request` structs. +/// +/// # 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, Body, Context, Error, IntoResponse, Request, RequestPayloadExt, Response, +/// }; +/// use serde::Deserialize; +/// +/// #[derive(Debug, Default, Deserialize)] +/// 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, 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 RequestPayloadExt { + /// Return the result of a payload parsed into a type that implements [`serde::Deserialize`] + /// + /// Currently only `application/x-www-form-urlencoded` + /// and `application/json` flavors of content type + /// are supported + /// + /// A [`PayloadError`] will be returned for undeserializable + /// payloads. If no body is provided, `Ok(None)` will be returned. + fn payload(&self) -> Result, PayloadError> + where + D: DeserializeOwned; +} + +impl RequestPayloadExt for http::Request { + fn payload(&self) -> Result, PayloadError> + where + for<'de> D: Deserialize<'de>, + { + self.headers() + .get(http::header::CONTENT_TYPE) + .map(|ct| match ct.to_str() { + Ok(content_type) => { + if content_type.starts_with("application/x-www-form-urlencoded") { + return serde_urlencoded::from_bytes::(self.body().as_ref()) + .map_err(PayloadError::WwwFormUrlEncoded) + .map(Some); + } else if content_type.starts_with("application/json") { + return serde_json::from_slice::(self.body().as_ref()) + .map_err(PayloadError::Json) + .map(Some); + } + + Ok(None) + } + _ => Ok(None), + }) + .unwrap_or_else(|| Ok(None)) + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::RequestPayloadExt; + + use crate::Body; + + #[derive(Deserialize, Eq, PartialEq, Debug)] + struct Payload { + foo: String, + baz: usize, + } + + #[test] + fn requests_have_form_post_parsable_payloads() { + let request = http::Request::builder() + .header("Content-Type", "application/x-www-form-urlencoded") + .body(Body::from("foo=bar&baz=2")) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_have_json_parsable_payloads() { + let request = http::Request::builder() + .header("Content-Type", "application/json") + .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_match_form_post_content_type_with_charset() { + let 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"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_match_json_content_type_with_charset() { + let 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"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_omitting_content_types_do_not_support_parsable_payloads() { + let request = http::Request::builder() + .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!(payload, None); + } +} diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index b4d9c5bd..37c167a0 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -32,11 +32,11 @@ //! //! ## Leveraging trigger provided data //! -//! You can also access information provided directly from the underlying trigger events, like query string parameters, -//! or Lambda function context, with the [`RequestExt`](trait.RequestExt.html) trait. +//! You can also access information provided directly from the underlying trigger events, +//! like query string parameters, or Lambda function context, with the [`RequestExt`] trait. //! //! ```rust,no_run -//! use lambda_http::{service_fn, Error, IntoResponse, Request, RequestExt}; +//! use lambda_http::{service_fn, Error, RequestExt, IntoResponse, Request}; //! //! #[tokio::main] //! async fn main() -> Result<(), Error> { @@ -47,13 +47,13 @@ //! async fn hello( //! request: Request //! ) -> Result { -//! let _context = request.lambda_context(); +//! let _context = request.lambda_context_ref(); //! //! Ok(format!( //! "hello {}", //! request -//! .query_string_parameters() -//! .first("name") +//! .query_string_parameters_ref() +//! .and_then(|params| params.first("name")) //! .unwrap_or_else(|| "stranger") //! )) //! } @@ -73,7 +73,10 @@ use response::ResponseFuture; pub mod ext; pub mod request; mod response; -pub use crate::{ext::RequestExt, response::IntoResponse}; +pub use crate::{ + ext::{RequestExt, RequestPayloadExt}, + response::IntoResponse, +}; use crate::{ request::{LambdaRequest, RequestOrigin}, response::LambdaResponse, diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs index c31398c5..2711a343 100644 --- a/lambda-http/src/request.rs +++ b/lambda-http/src/request.rs @@ -1,11 +1,14 @@ //! ALB and API Gateway request adaptations //! -//! Typically these are exposed via the `request_context` -//! request extension method provided by [lambda_http::RequestExt](../trait.RequestExt.html) +//! Typically these are exposed via the [`request_context()`] or [`request_context_ref()`] +//! request extension methods provided by the [`RequestExt`] trait. //! +//! [`request_context()`]: crate::RequestExt::request_context() +//! [`request_context_ref()`]: crate::RequestExt::request_context_ref() +//! [`RequestExt`]: crate::RequestExt #[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))] -use crate::ext::{PathParameters, StageVariables}; -use crate::ext::{QueryStringParameters, RawHttpPath}; +use crate::ext::extensions::{PathParameters, StageVariables}; +use crate::ext::extensions::{QueryStringParameters, RawHttpPath}; #[cfg(feature = "alb")] use aws_lambda_events::alb::{AlbTargetGroupRequest, AlbTargetGroupRequestContext}; #[cfg(feature = "apigw_rest")] @@ -447,7 +450,7 @@ fn build_request_uri( #[cfg(test)] mod tests { use super::*; - use crate::RequestExt; + use crate::ext::RequestExt; use std::fs::File; #[test] @@ -474,9 +477,9 @@ mod tests { assert_eq!(req.uri(), "https://xxx.execute-api.us-east-1.amazonaws.com/"); // Ensure this is an APIGWv2 request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::ApiGatewayV2(_)), + matches!(req_context, &RequestContext::ApiGatewayV2(_)), "expected ApiGatewayV2 context, got {req_context:?}" ); } @@ -503,9 +506,9 @@ mod tests { assert_eq!(cookie_header, Ok("cookie1=value1;cookie2=value2")); // Ensure this is an APIGWv2 request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::ApiGatewayV2(_)), + matches!(req_context, &RequestContext::ApiGatewayV2(_)), "expected ApiGatewayV2 context, got {req_context:?}" ); } @@ -529,9 +532,9 @@ mod tests { ); // Ensure this is an APIGW request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::ApiGatewayV1(_)), + matches!(req_context, &RequestContext::ApiGatewayV1(_)), "expected ApiGateway context, got {req_context:?}" ); } @@ -562,9 +565,9 @@ mod tests { assert_eq!(cookie_header, Some("test=hi".to_string())); // Ensure this is an APIGWv2 request (Lambda Function URL requests confirm to API GW v2 Request format) - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::ApiGatewayV2(_)), + matches!(req_context, &RequestContext::ApiGatewayV2(_)), "expected ApiGatewayV2 context, got {req_context:?}" ); } @@ -587,9 +590,9 @@ mod tests { ); // Ensure this is an ALB request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::Alb(_)), + matches!(req_context, &RequestContext::Alb(_)), "expected Alb context, got {req_context:?}" ); } @@ -612,9 +615,9 @@ mod tests { ); // Ensure this is an ALB request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::Alb(_)), + matches!(req_context, &RequestContext::Alb(_)), "expected Alb context, got {req_context:?}" ); } @@ -631,11 +634,16 @@ mod tests { ); let request = result.expect("failed to parse request"); - assert!(!request.query_string_parameters().is_empty()); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); - // test RequestExt#query_string_parameters does the right thing + // test RequestExt#query_string_parameters_ref does the right thing assert_eq!( - request.query_string_parameters().all("multivalueName"), + request + .query_string_parameters_ref() + .and_then(|params| params.all("multivalueName")), Some(vec!["you", "me"]) ); } @@ -651,11 +659,16 @@ mod tests { "event is was not parsed as expected {result:?} given {input}" ); let request = result.expect("failed to parse request"); - assert!(!request.query_string_parameters().is_empty()); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); - // test RequestExt#query_string_parameters does the right thing + // test RequestExt#query_string_parameters_ref does the right thing assert_eq!( - request.query_string_parameters().all("myKey"), + request + .query_string_parameters_ref() + .and_then(|params| params.all("myKey")), Some(vec!["val1", "val2"]) ); } @@ -671,11 +684,16 @@ mod tests { "event is was not parsed as expected {result:?} given {input}" ); let request = result.expect("failed to parse request"); - assert!(!request.query_string_parameters().is_empty()); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); - // test RequestExt#query_string_parameters does the right thing + // test RequestExt#query_string_parameters_ref does the right thing assert_eq!( - request.query_string_parameters().all("myKey"), + request + .query_string_parameters_ref() + .and_then(|params| params.all("myKey")), Some(vec!["?showAll=true", "?showAll=false"]) ); } diff --git a/lambda-http/src/streaming.rs b/lambda-http/src/streaming.rs index 150002be..9a27d915 100644 --- a/lambda-http/src/streaming.rs +++ b/lambda-http/src/streaming.rs @@ -1,6 +1,6 @@ -use crate::request::LambdaRequest; use crate::tower::ServiceBuilder; -use crate::{Request, RequestExt}; +use crate::Request; +use crate::{request::LambdaRequest, RequestExt}; pub use aws_lambda_events::encodings::Body as LambdaEventBody; use bytes::Bytes; pub use http::{self, Response}; diff --git a/lambda-integration-tests/src/bin/http-fn.rs b/lambda-integration-tests/src/bin/http-fn.rs index 4170d29f..cd252280 100644 --- a/lambda-integration-tests/src/bin/http-fn.rs +++ b/lambda-integration-tests/src/bin/http-fn.rs @@ -1,4 +1,4 @@ -use lambda_http::{service_fn, Body, Error, IntoResponse, Request, RequestExt, Response}; +use lambda_http::{ext::RequestExt, service_fn, Body, Error, IntoResponse, Request, Response}; use tracing::info; async fn handler(event: Request) -> Result { diff --git a/lambda-integration-tests/src/bin/http-trait.rs b/lambda-integration-tests/src/bin/http-trait.rs index fffe0db0..765b0d66 100644 --- a/lambda-integration-tests/src/bin/http-trait.rs +++ b/lambda-integration-tests/src/bin/http-trait.rs @@ -4,7 +4,7 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, }; -use lambda_http::{Body, Error, Request, RequestExt, Response, Service}; +use lambda_http::{ext::RequestExt, Body, Error, Request, Response, Service}; use tracing::info; struct MyHandler { diff --git a/lambda-runtime/src/types.rs b/lambda-runtime/src/types.rs index 31f4c8a2..87d6ded5 100644 --- a/lambda-runtime/src/types.rs +++ b/lambda-runtime/src/types.rs @@ -91,7 +91,7 @@ pub struct CognitoIdentity { /// The Lambda function execution context. The values in this struct /// are populated using the [Lambda environment variables](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html) -/// and the headers returned by the poll request to the Runtime APIs. +/// and [the headers returned by the poll request to the Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next). #[non_exhaustive] #[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)] pub struct Context {