Created
November 3, 2023 13:14
-
-
Save bassmanitram/3a22a0b98bfb59b5c5dad4be45888fb8 to your computer and use it in GitHub Desktop.
Serialize a Rust AWS API Gateway V1 Request back into its JSON representation, with no cloning
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// A JSON-serializeable representation of the V1 API Gateway event. | |
// | |
// While it is SUPER convenient to use the http::Request to handle the event in | |
// most of the Lambda code, when calling a Lambda function we need a JSON view of the | |
// event - i.e. we need to be able to recreate the JSON structure from the Request. | |
// aws_lambda_events::event::apigw::ApiGatewayProxyRequest already provides the structure | |
// and Serialize implementation to do this, BUT it requires an AWFUL lot of cloning to | |
// get the values in place. THIS approach uses references to the various parts from the | |
// Request and its extensions, so avoiding cloning where possible. | |
// | |
// This isn't too tough actually: | |
// | |
// * headers and multiValueHeaders come directly from Request::headers | |
// * queryStringParameters and multiValueQueryStringParameters come directly | |
// from the RequestExt::query_string_parameters | |
// * pathParameters come directly from RequestExt::path_parameters | |
// * stageVariables come directly from RequestExt::stage_variables | |
// * requestContext comes directly from RequestExt::request_context | |
// * resource comes from RequestExt::request_context.resource_path | |
// * path comes from RequestExt::request_context.path | |
// * httpMethod comes from RequestExt::request_context.http_method | |
// * body comes from lambda_http::Body in the request | |
// * isBase64Encoded is derived from the lambda_http::Body variant | |
// | |
// AND | |
// | |
use lambda_http::aws_lambda_events::{query_map::QueryMap, apigw::ApiGatewayProxyRequestContext}; | |
use http::HeaderMap; | |
use lambda_http::{ request::RequestContext, RequestExt, Request, Body}; | |
use serde::{Serialize, Serializer}; | |
// | |
// query_map::QueryMap has minimal serde support, so we do it "by hand" | |
// | |
fn query_map_single<S>(map: &QueryMap, ser: S) -> Result<S::Ok, S::Error> | |
where S: Serializer { | |
// | |
// This is inefficient in the case where the map has multiple values for a key | |
// because the key is produced multiple times | |
// | |
// https://github.com/calavera/query-map-rs/pull/15 | |
// | |
ser.collect_map(map | |
.iter() | |
.map(|(k, _)| (k, map.first(k).unwrap()))) | |
} | |
fn query_map_multi<S>(map: &QueryMap, ser: S) -> Result<S::Ok, S::Error> | |
where S: Serializer { | |
// | |
// This is inefficient in the case where the map has multiple values for a key | |
// because the key is produced multiple times | |
// | |
// https://github.com/calavera/query-map-rs/pull/15 | |
// | |
ser.collect_map(map | |
.iter() | |
.map(|(k, _)| (k, map.all(k).unwrap()))) | |
} | |
// | |
// HeaderMap isn't serializable, but it is more efficient than QueryMap to implement | |
// a custom serializer. | |
// | |
fn header_map_single<S>(map: &HeaderMap, ser: S) -> Result<S::Ok, S::Error> | |
where S: Serializer { | |
ser.collect_map(map | |
.keys() | |
.map(|k| (k.as_str(), map.get(k).unwrap().to_str().unwrap()))) | |
} | |
fn header_map_multi<S>(map: &HeaderMap, ser: S) -> Result<S::Ok, S::Error> | |
where S: Serializer { | |
ser.collect_map(map | |
.keys() | |
.map(|k| (k.as_str(), map.get_all(k).iter().map(|v|v.to_str().unwrap()).collect::<Vec<&str>>())) | |
) | |
} | |
#[derive(Serialize)] | |
#[serde(rename_all = "camelCase")] | |
struct ApiGatewayProxyRequest<'a> { | |
resource: Option<&'a String>, | |
path: Option<&'a String>, | |
http_method: &'a str, | |
body: &'a Body, | |
is_base64_encoded: bool, | |
#[serde(serialize_with = "query_map_single")] | |
query_string_parameters: QueryMap, | |
#[serde(serialize_with = "query_map_multi")] | |
multi_value_query_string_parameters: QueryMap, | |
#[serde(serialize_with = "query_map_single")] | |
path_parameters: QueryMap, | |
#[serde(serialize_with = "query_map_single")] | |
stage_variables: QueryMap, | |
#[serde(serialize_with = "header_map_single")] | |
headers: &'a HeaderMap, | |
#[serde(serialize_with = "header_map_multi")] | |
multi_value_headers: &'a HeaderMap, | |
request_context: &'a ApiGatewayProxyRequestContext, | |
} | |
pub fn to_api_gateway_proxy_request<'a>(request: &'a Request) -> Result<String,serde_json::Error> { | |
let request_context = match request.request_context_ref() { | |
Some(RequestContext::ApiGatewayV1(value)) => value, | |
_ => { panic!("501::Unsupported API request format"); } | |
}; | |
let agpr = ApiGatewayProxyRequest { | |
resource: request_context.resource_path.as_ref(), | |
path: request_context.path.as_ref(), | |
http_method: &request_context.http_method.as_str(), | |
body: request.body(), | |
is_base64_encoded: if let Body::Binary(_) = request.body() {true} else {false}, | |
query_string_parameters: request.query_string_parameters(), | |
multi_value_query_string_parameters: request.query_string_parameters(), | |
path_parameters: request.path_parameters(), | |
stage_variables: request.stage_variables(), | |
headers: request.headers(), | |
multi_value_headers: request.headers(), | |
request_context | |
}; | |
serde_json::to_string_pretty(&agpr) | |
} | |
#[cfg(test)] | |
mod tests { | |
use crate::utils::test_utils::create_request_from_file; | |
use super::to_api_gateway_proxy_request; | |
#[test] | |
fn text_body_test() { | |
// | |
// Parse a standard event JSON | |
// | |
let request_1 = create_request_from_file("test-request.json"); | |
// | |
// Reproduce it via Serde | |
// | |
let ser = to_api_gateway_proxy_request(&request_1); | |
assert!(ser.is_ok()); | |
// | |
// Reparse it - this is first level confirmation that the serialization is correct | |
// | |
let request_2 = lambda_http::request::from_str(&ser.unwrap()); | |
assert!(request_2.is_ok()); | |
// | |
// Now the hard bit - make sure that, semantically, request_1 and request_2 are the same | |
// | |
println!("{:#?}",request_2); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment