Skip to content

Instantly share code, notes, and snippets.

@bassmanitram
Created November 3, 2023 13:14
Show Gist options
  • Save bassmanitram/3a22a0b98bfb59b5c5dad4be45888fb8 to your computer and use it in GitHub Desktop.
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
//
// 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