From cf5b51a5eaa7f8eab52d06d710f242a0ca9c3fc9 Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Mon, 18 Aug 2025 15:16:01 +1000 Subject: [PATCH 1/8] Adding vpc lattice to Makefile --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index ecfd76231..b2ac52de3 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,7 @@ check-event-features: cargo test --package aws_lambda_events --no-default-features --features sns cargo test --package aws_lambda_events --no-default-features --features sqs cargo test --package aws_lambda_events --no-default-features --features streams + cargo test --package aws_lambda_events --no-default-features --features vpc_lattice fmt: cargo +nightly fmt --all \ No newline at end of file From 5b8fbfde91b8b7a5612f0fcba44c0ab4d0859b97 Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Mon, 18 Aug 2025 15:17:41 +1000 Subject: [PATCH 2/8] Added lambda-event structures --- lambda-events/Cargo.toml | 2 + lambda-events/src/event/mod.rs | 5 + lambda-events/src/event/vpc_lattice/mod.rs | 227 +++++++++++++++++++++ lambda-events/src/lib.rs | 5 + 4 files changed, 239 insertions(+) create mode 100644 lambda-events/src/event/vpc_lattice/mod.rs diff --git a/lambda-events/Cargo.toml b/lambda-events/Cargo.toml index cd2d86f2a..66a99cee2 100644 --- a/lambda-events/Cargo.toml +++ b/lambda-events/Cargo.toml @@ -78,6 +78,7 @@ default = [ "streams", "documentdb", "eventbridge", + "vpc_lattice" ] activemq = [] @@ -124,6 +125,7 @@ sqs = ["serde_with"] streams = [] documentdb = [] eventbridge = ["chrono", "serde_with"] +vpc_lattice = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"] catch-all-fields = [] diff --git a/lambda-events/src/event/mod.rs b/lambda-events/src/event/mod.rs index 275450fdd..44ead0bba 100644 --- a/lambda-events/src/event/mod.rs +++ b/lambda-events/src/event/mod.rs @@ -201,3 +201,8 @@ pub mod documentdb; #[cfg(feature = "eventbridge")] #[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] pub mod eventbridge; + +/// AWS Lambda event definitions for VPC Lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub mod vpc_lattice; diff --git a/lambda-events/src/event/vpc_lattice/mod.rs b/lambda-events/src/event/vpc_lattice/mod.rs new file mode 100644 index 000000000..946fb760f --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/mod.rs @@ -0,0 +1,227 @@ +use crate::{ + custom_serde::{ + deserialize_headers, deserialize_lambda_map, deserialize_nullish_boolean, http_method, serialize_headers, + serialize_multi_value_headers, + }, + encodings::Body, + iam::IamPolicyStatement, +}; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; +use std::collections::HashMap; + +/// `VpcLatticeRequest` contains data coming from VPC Lattice service +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequest { + /// The version of the event structure (always "2.0" for V2) + #[serde(default)] + pub version: Option, + /// The url path for the request + #[serde(default)] + pub path: Option, + /// The HTTP method of the request + #[serde(with = "http_method")] + pub method: Method, + /// HTTP headers of the request (VPC Lattice V2 uses arrays for multi-values) + #[serde(default, deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + /// HTTP query string parameters (VPC Lattice V2 uses arrays for multi-values) + #[serde( + default, + deserialize_with = "query_map::serde::aws_api_gateway_v2::deserialize_empty" + )] + #[serde(skip_serializing_if = "QueryMap::is_empty")] + #[serde(serialize_with = "query_map::serde::aws_api_gateway_v2::serialize_query_string_parameters")] + pub query_string_parameters: QueryMap, + /// The request body + #[serde(default)] + pub body: Option, + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + pub is_base64_encoded: bool, + /// VPC Lattice specific request context + #[serde(bound = "")] + pub request_context: VpcLatticeRequestContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// VPC Lattice specific request context +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestContext { + /// ARN of the service network that delivers the request + #[serde(default)] + pub service_network_arn: Option, + /// ARN of the service that receives the request + #[serde(default)] + pub service_arn: Option, + /// ARN of the target group that receives the request + #[serde(default)] + pub target_group_arn: Option, + /// Identity information for the request + #[serde(default)] + pub identity: Option, + /// AWS region where the request is processed + #[serde(default)] + pub region: Option, + /// Time of the request in microseconds since epoch + #[serde(default)] + pub time_epoch: Option, + /// Catchall for additional context fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// Identity information in VPC Lattice request context +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeIdentity { + /// ARN of the VPC where the request originated + #[serde(default)] + pub source_vpc_arn: Option, + /// Type of authentication (e.g., "AWS_IAM") + #[serde(rename = "type")] + #[serde(default)] + pub identity_type: Option, + /// The authenticated principal + #[serde(default)] + pub principal: Option, + /// Organization ID of the authenticated principal + #[serde(rename = "principalOrgID")] + #[serde(default)] + pub principal_org_id: Option, + /// Name of the authenticated session + #[serde(default)] + pub session_name: Option, + /// X.509 certificate fields (for Roles Anywhere) + #[serde(rename = "x509IssuerOu")] + #[serde(default)] + pub x509_issuer_ou: Option, + #[serde(rename = "x509SanDns")] + #[serde(default)] + pub x509_san_dns: Option, + #[serde(rename = "x509SanNameCn")] + #[serde(default)] + pub x509_san_name_cn: Option, + #[serde(rename = "x509SanUri")] + #[serde(default)] + pub x509_san_uri: Option, + #[serde(rename = "x509SubjectCn")] + #[serde(default)] + pub x509_subject_cn: Option, + /// Catchall for additional identity fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// `VpcLatticeResponse` configures the response to be returned by VPC Lattice for the request +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeResponse { + /// The HTTP status code for the request + pub status_code: u16, + /// The HTTP status description (optional) + #[serde(default)] + pub status_description: Option, + /// The Http headers to return + #[serde(deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_headers")] + #[serde(skip_serializing_if = "HeaderMap::is_empty")] + #[serde(default)] + pub headers: HeaderMap, + /// The response body + #[serde(default)] + pub body: Option, + /// Whether the body is base64 encoded + #[serde(default)] + pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/* +/// Custom deserializer for VPC Lattice headers (which use arrays for multiple values) +fn deserialize_vpc_lattice_headers<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let headers: HashMap> = HashMap::deserialize(deserializer)?; + let mut header_map = HeaderMap::new(); + + for (key, values) in headers { + let header_name = HeaderName::from_str(&key).map_err(serde::de::Error::custom)?; + for value in values { + let header_value = HeaderValue::from_str(&value).map_err(serde::de::Error::custom)?; + header_map.append(header_name.clone(), header_value); + } + } + + Ok(header_map) +} + +/// Custom serializer for VPC Lattice headers +fn serialize_vpc_lattice_headers(headers: &HeaderMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = HashMap::>::new(); + + for (name, value) in headers { + let key = name.as_str().to_string(); + let value_str = value.to_str().unwrap_or("").to_string(); + map.entry(key).or_insert_with(Vec::new).push(value_str); + } + + map.serialize(serializer) +} + +/// Custom deserializer for VPC Lattice query parameters (which use arrays for multiple values) +fn deserialize_vpc_lattice_query_params<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let params: HashMap> = HashMap::deserialize(deserializer)?; + let mut query_map = QueryMap::new(); + + for (key, values) in params { + query_map.insert(key, values); + } + + Ok(query_map) +} + +fn serialize_vpc_lattice_query_params(params: &QueryMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = HashMap::>::new(); + + for (name, value) in params { + let key = name.as_str().to_string(); + let value_str = value.to_str().unwrap_or("").to_string(); + map.entry(key).or_insert_with(Vec::new).push(value_str); + } + + map.serialize(serializer) +} + + */ \ No newline at end of file diff --git a/lambda-events/src/lib.rs b/lambda-events/src/lib.rs index d35dbd760..380670994 100644 --- a/lambda-events/src/lib.rs +++ b/lambda-events/src/lib.rs @@ -226,3 +226,8 @@ pub use event::documentdb; #[cfg(feature = "eventbridge")] #[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] pub use event::eventbridge; + +/// AWS Lambda event definitions for VPC lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub use event::vpc_lattice; From f51796204e64645c46754561399ed022ee11c407 Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Fri, 29 Aug 2025 10:49:37 +1000 Subject: [PATCH 3/8] First cut of new event jsons and structures --- lambda-events/src/event/vpc_lattice/common.rs | 0 .../serialization_comma_separated_headers.rs | 173 ++++++++++++++++++ lambda-events/src/event/vpc_lattice/v1.rs | 0 lambda-events/src/event/vpc_lattice/v2.rs | 0 .../example-vpc-lattice-v1-request.json | 0 ...example-vpc-lattice-v2-request-base64.json | 0 ...attice-v2-request-multi-value-headers.json | 0 ...vpc-lattice-v2-request-roles-anywhere.json | 0 .../example-vpc-lattice-v2-request.json | 0 .../example-vpc-lattice-v2-response.json | 0 10 files changed, 173 insertions(+) create mode 100644 lambda-events/src/event/vpc_lattice/common.rs create mode 100644 lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs create mode 100644 lambda-events/src/event/vpc_lattice/v1.rs create mode 100644 lambda-events/src/event/vpc_lattice/v2.rs create mode 100644 lambda-events/src/fixtures/example-vpc-lattice-v1-request.json create mode 100644 lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json create mode 100644 lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json create mode 100644 lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json create mode 100644 lambda-events/src/fixtures/example-vpc-lattice-v2-request.json create mode 100644 lambda-events/src/fixtures/example-vpc-lattice-v2-response.json diff --git a/lambda-events/src/event/vpc_lattice/common.rs b/lambda-events/src/event/vpc_lattice/common.rs new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs new file mode 100644 index 000000000..41625506f --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs @@ -0,0 +1,173 @@ +use http::{header::HeaderName, HeaderMap, HeaderValue}; +use serde::{ + de::{self, Deserializer, Error as DeError, MapAccess, Unexpected, Visitor}, + ser::{Error as SerError, SerializeMap, Serializer}, +}; +use std::{borrow::Cow, fmt}; + +/// Implementation detail. +pub(crate) fn deserialize_comma_separated_headers<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let is_human_readable = de.is_human_readable(); + de.deserialize_option(HeaderMapVisitor { is_human_readable }) +} + +#[derive(serde::Deserialize)] +#[serde(untagged)] +enum OneOrMore<'a> { + One(Cow<'a, str>), + Strings(Vec>), + Bytes(Vec>), + CommaSeparated(Cow<'a, str>), +} + +struct HeaderMapVisitor { + is_human_readable: bool, +} + +impl<'de> Visitor<'de> for HeaderMapVisitor { + type Value = HeaderMap; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("lots of things can go wrong with HeaderMap") + } + + fn visit_unit(self) -> Result + where + E: DeError, + { + Ok(HeaderMap::default()) + } + + fn visit_none(self) -> Result + where + E: DeError, + { + Ok(HeaderMap::default()) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(self) + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = HeaderMap::with_capacity(access.size_hint().unwrap_or(0)); + + if !self.is_human_readable { + while let Some((key, arr)) = access.next_entry::, Vec>>()? { + let key = HeaderName::from_bytes(key.as_bytes()) + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&key), &self))?; + for val in arr { + let val = HeaderValue::from_bytes(&val) + .map_err(|_| de::Error::invalid_value(Unexpected::Bytes(&val), &self))?; + map.append(&key, val); + } + } + } else { + while let Some((key, val)) = access.next_entry::, OneOrMore<'_>>()? { + let key = HeaderName::from_bytes(key.as_bytes()) + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&key), &self))?; + match val { + OneOrMore::One(val) => { + // Check if the single value contains commas and split if needed + if val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { + let header_val = val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; + map.insert(key, header_val); + } + } + OneOrMore::Strings(arr) => { + for val in arr { + // Each string in the array might also be comma-separated + if val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { + let header_val = val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; + map.append(&key, header_val); + } + } + } + OneOrMore::Bytes(arr) => { + for val in arr { + let header_val = HeaderValue::from_bytes(&val) + .map_err(|_| de::Error::invalid_value(Unexpected::Bytes(&val), &self))?; + map.append(&key, header_val); + } + } + OneOrMore::CommaSeparated(val) => { + // Explicitly handle comma-separated values + split_and_append_header(&mut map, &key, &val, &self)?; + } + }; + } + } + Ok(map) + } +} + +fn split_and_append_header( + map: &mut HeaderMap, + key: &HeaderName, + value: &str, + visitor: &HeaderMapVisitor +) -> Result<(), E> +where + E: DeError, +{ + for split_val in value.split(',') { + let trimmed_val = split_val.trim(); + if !trimmed_val.is_empty() { // Skip empty values from trailing commas + let header_val = trimmed_val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(trimmed_val), visitor))?; + map.append(key, header_val); + } + } + Ok(()) +} + +#[derive(Debug)] +pub struct FixedString(String); + +impl<'de> Deserialize<'de> for FixedString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_string(FixedStringVisitor) + } +} + +struct FixedStringVisitor; + +impl<'de> Visitor<'de> for FixedStringVisitor { + type Value = FixedString; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a fixed string \"2.0\"") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value == "2.0" { + Ok(FixedString(value.to_owned())) + } else { + Err(E::custom(format!("unexpected string value: {}", value))) + } + } +} \ No newline at end of file diff --git a/lambda-events/src/event/vpc_lattice/v1.rs b/lambda-events/src/event/vpc_lattice/v1.rs new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json new file mode 100644 index 000000000..e69de29bb diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-response.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-response.json new file mode 100644 index 000000000..e69de29bb From 2fcd18d555167c07f5622cdfc88c8929cb0d8c8b Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Fri, 29 Aug 2025 10:50:29 +1000 Subject: [PATCH 4/8] fmt --- lambda-events/src/event/vpc_lattice/common.rs | 42 +++ lambda-events/src/event/vpc_lattice/mod.rs | 239 +----------------- .../serialization_comma_separated_headers.rs | 127 +++++++--- lambda-events/src/event/vpc_lattice/v1.rs | 88 +++++++ lambda-events/src/event/vpc_lattice/v2.rs | 236 +++++++++++++++++ .../example-vpc-lattice-v1-request.json | 19 ++ ...example-vpc-lattice-v2-request-base64.json | 29 +++ ...attice-v2-request-multi-value-headers.json | 34 +++ ...vpc-lattice-v2-request-roles-anywhere.json | 37 +++ .../example-vpc-lattice-v2-request.json | 30 +++ .../example-vpc-lattice-v2-response.json | 11 + 11 files changed, 631 insertions(+), 261 deletions(-) diff --git a/lambda-events/src/event/vpc_lattice/common.rs b/lambda-events/src/event/vpc_lattice/common.rs index e69de29bb..2626ff18d 100644 --- a/lambda-events/src/event/vpc_lattice/common.rs +++ b/lambda-events/src/event/vpc_lattice/common.rs @@ -0,0 +1,42 @@ +use crate::custom_serde::{deserialize_headers, serialize_headers}; +use http::HeaderMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `VpcLatticeResponse` configures the response to be returned +/// by VPC Lattice (both V1 and V2) for the request +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeResponse { + // https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#respond-to-service + /// Whether the body is base64 encoded + #[serde(default)] + pub is_base64_encoded: bool, + + /// The HTTP status code for the request + pub status_code: u16, + + /// The HTTP status description (optional) + #[serde(default)] + pub status_description: Option, + + /// The Http headers to return + #[serde(deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_headers")] + #[serde(skip_serializing_if = "HeaderMap::is_empty")] + #[serde(default)] + pub headers: HeaderMap, + + /// The response body + #[serde(default)] + pub body: Option, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/vpc_lattice/mod.rs b/lambda-events/src/event/vpc_lattice/mod.rs index 946fb760f..c8668b5bc 100644 --- a/lambda-events/src/event/vpc_lattice/mod.rs +++ b/lambda-events/src/event/vpc_lattice/mod.rs @@ -1,227 +1,12 @@ -use crate::{ - custom_serde::{ - deserialize_headers, deserialize_lambda_map, deserialize_nullish_boolean, http_method, serialize_headers, - serialize_multi_value_headers, - }, - encodings::Body, - iam::IamPolicyStatement, -}; -use http::{HeaderMap, Method}; -use query_map::QueryMap; -use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::Value; -use std::collections::HashMap; - -/// `VpcLatticeRequest` contains data coming from VPC Lattice service -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VpcLatticeRequest { - /// The version of the event structure (always "2.0" for V2) - #[serde(default)] - pub version: Option, - /// The url path for the request - #[serde(default)] - pub path: Option, - /// The HTTP method of the request - #[serde(with = "http_method")] - pub method: Method, - /// HTTP headers of the request (VPC Lattice V2 uses arrays for multi-values) - #[serde(default, deserialize_with = "deserialize_headers")] - #[serde(serialize_with = "serialize_headers")] - pub headers: HeaderMap, - /// HTTP query string parameters (VPC Lattice V2 uses arrays for multi-values) - #[serde( - default, - deserialize_with = "query_map::serde::aws_api_gateway_v2::deserialize_empty" - )] - #[serde(skip_serializing_if = "QueryMap::is_empty")] - #[serde(serialize_with = "query_map::serde::aws_api_gateway_v2::serialize_query_string_parameters")] - pub query_string_parameters: QueryMap, - /// The request body - #[serde(default)] - pub body: Option, - /// Whether the body is base64 encoded - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] - pub is_base64_encoded: bool, - /// VPC Lattice specific request context - #[serde(bound = "")] - pub request_context: VpcLatticeRequestContext, - /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. - /// Enabled with Cargo feature `catch-all-fields`. - /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. - #[cfg(feature = "catch-all-fields")] - #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] - #[serde(flatten)] - pub other: serde_json::Map, -} - -/// VPC Lattice specific request context -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VpcLatticeRequestContext { - /// ARN of the service network that delivers the request - #[serde(default)] - pub service_network_arn: Option, - /// ARN of the service that receives the request - #[serde(default)] - pub service_arn: Option, - /// ARN of the target group that receives the request - #[serde(default)] - pub target_group_arn: Option, - /// Identity information for the request - #[serde(default)] - pub identity: Option, - /// AWS region where the request is processed - #[serde(default)] - pub region: Option, - /// Time of the request in microseconds since epoch - #[serde(default)] - pub time_epoch: Option, - /// Catchall for additional context fields - #[cfg(feature = "catch-all-fields")] - #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] - #[serde(flatten)] - pub other: serde_json::Map, -} - -/// Identity information in VPC Lattice request context -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VpcLatticeIdentity { - /// ARN of the VPC where the request originated - #[serde(default)] - pub source_vpc_arn: Option, - /// Type of authentication (e.g., "AWS_IAM") - #[serde(rename = "type")] - #[serde(default)] - pub identity_type: Option, - /// The authenticated principal - #[serde(default)] - pub principal: Option, - /// Organization ID of the authenticated principal - #[serde(rename = "principalOrgID")] - #[serde(default)] - pub principal_org_id: Option, - /// Name of the authenticated session - #[serde(default)] - pub session_name: Option, - /// X.509 certificate fields (for Roles Anywhere) - #[serde(rename = "x509IssuerOu")] - #[serde(default)] - pub x509_issuer_ou: Option, - #[serde(rename = "x509SanDns")] - #[serde(default)] - pub x509_san_dns: Option, - #[serde(rename = "x509SanNameCn")] - #[serde(default)] - pub x509_san_name_cn: Option, - #[serde(rename = "x509SanUri")] - #[serde(default)] - pub x509_san_uri: Option, - #[serde(rename = "x509SubjectCn")] - #[serde(default)] - pub x509_subject_cn: Option, - /// Catchall for additional identity fields - #[cfg(feature = "catch-all-fields")] - #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] - #[serde(flatten)] - pub other: serde_json::Map, -} - -/// `VpcLatticeResponse` configures the response to be returned by VPC Lattice for the request -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VpcLatticeResponse { - /// The HTTP status code for the request - pub status_code: u16, - /// The HTTP status description (optional) - #[serde(default)] - pub status_description: Option, - /// The Http headers to return - #[serde(deserialize_with = "deserialize_headers")] - #[serde(serialize_with = "serialize_headers")] - #[serde(skip_serializing_if = "HeaderMap::is_empty")] - #[serde(default)] - pub headers: HeaderMap, - /// The response body - #[serde(default)] - pub body: Option, - /// Whether the body is base64 encoded - #[serde(default)] - pub is_base64_encoded: bool, - /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. - /// Enabled with Cargo feature `catch-all-fields`. - /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. - #[cfg(feature = "catch-all-fields")] - #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] - #[serde(flatten)] - pub other: serde_json::Map, -} - -/* -/// Custom deserializer for VPC Lattice headers (which use arrays for multiple values) -fn deserialize_vpc_lattice_headers<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let headers: HashMap> = HashMap::deserialize(deserializer)?; - let mut header_map = HeaderMap::new(); - - for (key, values) in headers { - let header_name = HeaderName::from_str(&key).map_err(serde::de::Error::custom)?; - for value in values { - let header_value = HeaderValue::from_str(&value).map_err(serde::de::Error::custom)?; - header_map.append(header_name.clone(), header_value); - } - } - - Ok(header_map) -} - -/// Custom serializer for VPC Lattice headers -fn serialize_vpc_lattice_headers(headers: &HeaderMap, serializer: S) -> Result -where - S: Serializer, -{ - let mut map = HashMap::>::new(); - - for (name, value) in headers { - let key = name.as_str().to_string(); - let value_str = value.to_str().unwrap_or("").to_string(); - map.entry(key).or_insert_with(Vec::new).push(value_str); - } - - map.serialize(serializer) -} - -/// Custom deserializer for VPC Lattice query parameters (which use arrays for multiple values) -fn deserialize_vpc_lattice_query_params<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let params: HashMap> = HashMap::deserialize(deserializer)?; - let mut query_map = QueryMap::new(); - - for (key, values) in params { - query_map.insert(key, values); - } - - Ok(query_map) -} - -fn serialize_vpc_lattice_query_params(params: &QueryMap, serializer: S) -> Result -where - S: Serializer, -{ - let mut map = HashMap::>::new(); - - for (name, value) in params { - let key = name.as_str().to_string(); - let value_str = value.to_str().unwrap_or("").to_string(); - map.entry(key).or_insert_with(Vec::new).push(value_str); - } - - map.serialize(serializer) -} - - */ \ No newline at end of file +mod common; +mod serialization_comma_separated_headers; +mod v1; +mod v2; + +// re-export types +pub use self::common::*; +pub use self::v1::*; +pub use self::v2::*; + +// helper code +pub(crate) use self::serialization_comma_separated_headers::*; diff --git a/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs index 41625506f..f04c313f8 100644 --- a/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs +++ b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs @@ -1,11 +1,11 @@ use http::{header::HeaderName, HeaderMap, HeaderValue}; use serde::{ de::{self, Deserializer, Error as DeError, MapAccess, Unexpected, Visitor}, - ser::{Error as SerError, SerializeMap, Serializer}, + ser::{SerializeMap, Serializer}, }; use std::{borrow::Cow, fmt}; -/// Implementation detail. +/// Deserialize (potentially) comma separated headers into a HeaderMap pub(crate) fn deserialize_comma_separated_headers<'de, D>(de: D) -> Result where D: Deserializer<'de>, @@ -14,6 +14,33 @@ where de.deserialize_option(HeaderMapVisitor { is_human_readable }) } +/// Serialize a HeaderMap with multiple values per header combined as comma-separated strings +pub(crate) fn serialize_comma_separated_headers(headers: &HeaderMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(headers.keys_len()))?; + + // Group headers by name and combine values + for key in headers.keys() { + let values: Vec<&str> = headers + .get_all(key) + .iter() + .filter_map(|v| v.to_str().ok()) // Skip invalid UTF-8 values + .collect(); + + if !values.is_empty() { + let combined_value = values.join(", "); + map.serialize_entry(key.as_str(), &combined_value)?; + } + } + + map.end() +} + +// extension/duplicate of existing code from custom_serde/headers.rs +// could possibly be refactored back into common code + #[derive(serde::Deserialize)] #[serde(untagged)] enum OneOrMore<'a> { @@ -34,13 +61,6 @@ impl<'de> Visitor<'de> for HeaderMapVisitor { formatter.write_str("lots of things can go wrong with HeaderMap") } - fn visit_unit(self) -> Result - where - E: DeError, - { - Ok(HeaderMap::default()) - } - fn visit_none(self) -> Result where E: DeError, @@ -55,6 +75,13 @@ impl<'de> Visitor<'de> for HeaderMapVisitor { deserializer.deserialize_map(self) } + fn visit_unit(self) -> Result + where + E: DeError, + { + Ok(HeaderMap::default()) + } + fn visit_map(self, mut access: M) -> Result where M: MapAccess<'de>, @@ -122,14 +149,15 @@ fn split_and_append_header( map: &mut HeaderMap, key: &HeaderName, value: &str, - visitor: &HeaderMapVisitor + visitor: &HeaderMapVisitor, ) -> Result<(), E> where E: DeError, { for split_val in value.split(',') { let trimmed_val = split_val.trim(); - if !trimmed_val.is_empty() { // Skip empty values from trailing commas + if !trimmed_val.is_empty() { + // Skip empty values from trailing commas let header_val = trimmed_val .parse() .map_err(|_| de::Error::invalid_value(Unexpected::Str(trimmed_val), visitor))?; @@ -139,35 +167,66 @@ where Ok(()) } -#[derive(Debug)] -pub struct FixedString(String); +#[cfg(test)] +mod tests { + use super::*; + use http::{HeaderMap, HeaderValue}; + use serde_json; + use serde_with::serde_derive::Deserialize; + use serde_with::serde_derive::Serialize; + + #[test] + fn test_function_deserializer() { + #[derive(Deserialize)] + struct RequestWithHeaders { + #[serde(deserialize_with = "deserialize_comma_separated_headers")] + headers: HeaderMap, + } -impl<'de> Deserialize<'de> for FixedString { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_string(FixedStringVisitor) + let r: RequestWithHeaders = + serde_json::from_str("{ \"headers\": {\"x-foo\": \"z\", \"x-multi\": \"abcd, DEF, w\" }}").unwrap(); + + assert_eq!("z", r.headers.get_all("x-foo").iter().nth(0).unwrap()); + assert_eq!("abcd", r.headers.get_all("x-multi").iter().nth(0).unwrap()); + assert_eq!("DEF", r.headers.get_all("x-multi").iter().nth(1).unwrap()); + assert_eq!("w", r.headers.get_all("x-multi").iter().nth(2).unwrap()); } -} -struct FixedStringVisitor; + fn create_test_headermap() -> HeaderMap { + let mut headers = HeaderMap::new(); -impl<'de> Visitor<'de> for FixedStringVisitor { - type Value = FixedString; + // Single value header + headers.insert("content-type", HeaderValue::from_static("application/json")); - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a fixed string \"2.0\"") + // Multiple value header + headers.append("accept", HeaderValue::from_static("text/html")); + headers.append("accept", HeaderValue::from_static("application/json")); + headers.append("accept", HeaderValue::from_static("*/*")); + + // Another multiple value header + headers.append("cache-control", HeaderValue::from_static("no-cache")); + headers.append("cache-control", HeaderValue::from_static("must-revalidate")); + + headers } - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - if value == "2.0" { - Ok(FixedString(value.to_owned())) - } else { - Err(E::custom(format!("unexpected string value: {}", value))) + #[test] + fn test_function_serializer() { + #[derive(Serialize)] + struct RequestWithHeaders { + #[serde(serialize_with = "serialize_comma_separated_headers")] + headers: HeaderMap, + body: String, } + + let request = RequestWithHeaders { + headers: create_test_headermap(), + body: "test body".to_string(), + }; + + let json = serde_json::to_string_pretty(&request).unwrap(); + + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert!(parsed["headers"]["accept"].as_str().unwrap().contains(", ")); } -} \ No newline at end of file +} diff --git a/lambda-events/src/event/vpc_lattice/v1.rs b/lambda-events/src/event/vpc_lattice/v1.rs index e69de29bb..3125804d6 100644 --- a/lambda-events/src/event/vpc_lattice/v1.rs +++ b/lambda-events/src/event/vpc_lattice/v1.rs @@ -0,0 +1,88 @@ +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use crate::custom_serde::{deserialize_nullish_boolean, http_method}; +use crate::vpc_lattice::{deserialize_comma_separated_headers, serialize_comma_separated_headers}; + +/// `VpcLatticeRequestV1` contains data coming from VPC Lattice service (V1 format) +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +// we note that V1 requests are snake cased UNLIKE v2 which are camel cased +#[serde(rename_all = "snake_case")] +pub struct VpcLatticeRequestV1 { + /// The url path for the request + #[serde(default)] + pub raw_path: Option, + + /// The HTTP method of the request + #[serde(with = "http_method")] + pub method: Method, + + /// HTTP headers of the request (V1 uses comma-separated strings for multi-values) + #[serde(deserialize_with = "deserialize_comma_separated_headers", default)] + #[serde(serialize_with = "serialize_comma_separated_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (V1 uses the last value passed for multi-values + /// so no special serializer is needed) + #[serde(default)] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + pub is_base64_encoded: bool, + + /// Catchall to catch any additional fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("/api/product", parsed.raw_path.unwrap()); + assert_eq!("POST", parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().nth(0).unwrap() + ); + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!("{\"id\": 5, \"description\": \"TV\"}", parsed.body.unwrap()); + assert_eq!(false, parsed.is_base64_encoded); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_headers_multi_values() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("abcd", parsed.headers.get_all("multi").iter().nth(0).unwrap()); + assert_eq!("DEF", parsed.headers.get_all("multi").iter().nth(1).unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_query_string_map() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!("tv", parsed.query_string_parameters.first("tags").unwrap()); + } +} diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs index e69de29bb..afcfc4f95 100644 --- a/lambda-events/src/event/vpc_lattice/v2.rs +++ b/lambda-events/src/event/vpc_lattice/v2.rs @@ -0,0 +1,236 @@ +use crate::custom_serde::{deserialize_headers, deserialize_nullish_boolean, http_method, serialize_headers}; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +// field ordering and types matched to example in +// https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#receive-event-from-service + +/// `VpcLatticeRequestV2` contains data coming from VPC Lattice service (V2 format) +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2 { + /// The version of the event structure (always "2.0" for V2) + #[serde(default)] + pub version: Option, + + /// The url path for the request + #[serde(default)] + pub path: Option, + + /// The HTTP method of the request + #[serde(with = "http_method")] + pub method: Method, + + /// HTTP headers of the request (VPC Lattice V2 uses arrays for multi-values) + #[serde(default, deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (VPC Lattice V2 uses arrays for multi-values) + #[serde( + default, + deserialize_with = "query_map::serde::aws_api_gateway_v2::deserialize_empty" + )] + #[serde(skip_serializing_if = "QueryMap::is_empty")] + #[serde(serialize_with = "query_map::serde::aws_api_gateway_v2::serialize_query_string_parameters")] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + pub is_base64_encoded: bool, + + /// VPC Lattice specific request context + #[serde(bound = "")] + pub request_context: VpcLatticeRequestV2Context, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// VPC Lattice specific request context +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Context { + /// ARN of the service network that delivers the request + #[serde(default)] + pub service_network_arn: Option, + + /// ARN of the service that receives the request + #[serde(default)] + pub service_arn: Option, + + /// ARN of the target group that receives the request + #[serde(default)] + pub target_group_arn: Option, + + /// Identity information for the request + #[serde(default)] + pub identity: Option, + + /// AWS region where the request is processed + #[serde(default)] + pub region: Option, + + /// Time of the request in microseconds since epoch + #[serde(default)] + pub time_epoch: Option, + + /// Catchall for additional context fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// Identity information in VPC Lattice request context +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Identity { + /// ARN of the VPC where the request originated + #[serde(default)] + pub source_vpc_arn: Option, + + /// Type of authentication (e.g., "AWS_IAM") + #[serde(rename = "type")] + #[serde(default)] + pub identity_type: Option, + + /// The authenticated principal + #[serde(default)] + pub principal: Option, + + /// Organization ID of the authenticated principal + #[serde(rename = "principalOrgID")] + #[serde(default)] + pub principal_org_id: Option, + + /// Name of the authenticated session + #[serde(default)] + pub session_name: Option, + + /// X.509 certificate fields (for Roles Anywhere) + #[serde(rename = "x509IssuerOu")] + #[serde(default)] + pub x509_issuer_ou: Option, + #[serde(rename = "x509SanDns")] + #[serde(default)] + pub x509_san_dns: Option, + #[serde(rename = "x509SanNameCn")] + #[serde(default)] + pub x509_san_name_cn: Option, + #[serde(rename = "x509SanUri")] + #[serde(default)] + pub x509_san_uri: Option, + #[serde(rename = "x509SubjectCn")] + #[serde(default)] + pub x509_subject_cn: Option, + + /// Catchall for additional identity fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + + assert_eq!("/health", parsed.path.unwrap()); + assert_eq!("GET", parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().nth(0).unwrap() + ); + + let header_multi = parsed.headers.get_all("multi"); + assert_eq!("x", header_multi.iter().nth(0).unwrap()); + assert_eq!("y", header_multi.iter().nth(1).unwrap()); + + assert_eq!("prod", parsed.query_string_parameters.first("state").unwrap()); + let query_multi = parsed.query_string_parameters.all("multi").unwrap(); + assert_eq!(&"a", query_multi.iter().nth(0).unwrap()); + assert_eq!(&"DEF", query_multi.iter().nth(1).unwrap()); + assert_eq!(&"g", query_multi.iter().nth(2).unwrap()); + + assert!(parsed.body.is_none()); + assert_eq!(false, parsed.is_base64_encoded); + + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + parsed.request_context.service_arn.unwrap() + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + parsed.request_context.service_network_arn.unwrap() + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + parsed.request_context.target_group_arn.unwrap() + ); + assert_eq!( + "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + parsed.request_context.identity.unwrap().source_vpc_arn.unwrap() + ); + //assert_eq!("", parsed.request_context.service_arn.unwrap()); + //assert_eq!("", parsed.request_context.service_arn.unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_basic() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_base64_body() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-base64.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_multi_value_headers() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-multi-value-headers.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_role_anywhere() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-roles-anywhere.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json index e69de29bb..bda2de8f9 100644 --- a/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json +++ b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json @@ -0,0 +1,19 @@ +{ + "raw_path": "/api/product", + "method": "POST", + "headers": { + "accept": "*/*", + "user-agent": "curl/7.68.0", + "x-forwarded-for": "10.0.2.100", + "authorization": "Bearer abc123def456", + "multi": "abcd, DEF" + }, + "query_string_parameters": { + "category": "electronics", + "sort": "price", + "limit": "10", + "tags": "tv" + }, + "body": "{\"id\": 5, \"description\": \"TV\"}", + "is_base64_encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json index e69de29bb..8cf238b64 100644 --- a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json @@ -0,0 +1,29 @@ +{ + "version": "2.0", + "path": "/api/files/upload", + "method": "POST", + "headers": { + "content-type": ["multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"], + "content-encoding": ["gzip"], + "content-length": ["1024"], + "x-forwarded-for": ["10.0.1.45"] + }, + "queryStringParameters": { + "uploadType": ["profile-image"] + }, + "body": "H4sIAAAAAAAAA+3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA", + "isBase64Encoded": true, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:user/john.developer", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875299234567" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json index e69de29bb..a7d4c04af 100644 --- a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json @@ -0,0 +1,34 @@ +{ + "version": "2.0", + "path": "/api/users/123/orders", + "method": "POST", + "headers": { + "content-type": ["application/json"], + "authorization": ["Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."], + "user-agent": ["MyApp/1.0"], + "x-forwarded-for": ["10.0.1.45"], + "x-custom-header": ["value1", "value2"], + "accept": ["application/json", "application/xml"] + }, + "queryStringParameters": { + "include": ["metadata", "shipping"], + "format": ["json"], + "debug": ["true"] + }, + "body": "{\"productId\": \"prod-456\", \"quantity\": 2, \"shippingAddress\": {\"street\": \"123 Main St\", \"city\": \"Melbourne\", \"state\": \"VIC\", \"postcode\": \"3000\"}}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:assumed-role/OrderService-ExecutionRole/i-0c7de02a688bde9f7", + "principalOrgID": "o-50dc6c495c0c9188", + "sessionName": "i-0c7de02a688bde9f7" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875199177430" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json index e69de29bb..f4d1dbb40 100644 --- a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json @@ -0,0 +1,37 @@ +{ + "version": "2.0", + "path": "/api/external/data-sync", + "method": "POST", + "headers": { + "content-type": ["application/json"], + "user-agent": ["ExternalDataSync/2.1.0"], + "x-forwarded-for": ["203.45.67.89"], + "x-client-cert": ["present"], + "authorization": ["X509-Cert"] + }, + "queryStringParameters": { + "sync-type": ["incremental"], + "validate": ["true"] + }, + "body": "{\"timestamp\": \"2025-08-28T14:30:00Z\", \"records\": [{\"id\": \"ext-001\", \"data\": \"sample-data\"}], \"source\": \"external-partner-system\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/ExternalPartnerAccessRole", + "principalOrgID": "o-50dc6c495c0c9188", + "sessionName": "external-data-sync-session", + "x509IssuerOu": "Engineering Department", + "x509SanDns": "partner-system.external-company.com", + "x509SanNameCn": "Data Sync Service", + "x509SanUri": "https://partner-system.external-company.com/sync", + "x509SubjectCn": "external-partner-data-sync.external-company.com" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875199177430" + } +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json index e69de29bb..6901a37bd 100644 --- a/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": null, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-response.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-response.json index e69de29bb..70a95cbf9 100644 --- a/lambda-events/src/fixtures/example-vpc-lattice-v2-response.json +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-response.json @@ -0,0 +1,11 @@ +{ + "statusCode": 200, + "statusDescription": "200 OK", + "headers": { + "content-type": "application/json", + "x-request-id": "req-123456789", + "cache-control": "no-cache" + }, + "body": "{\"orderId\": \"order-789\", \"status\": \"created\", \"message\": \"Order successfully created\"}", + "isBase64Encoded": false +} \ No newline at end of file From 0935e843af0ca1709236648dffddb362a943ff6e Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Fri, 29 Aug 2025 11:17:53 +1000 Subject: [PATCH 5/8] fix header serialize --- lambda-events/src/event/vpc_lattice/v2.rs | 61 +++++++++++-------- ...attice-v2-request-multi-value-headers.json | 34 ----------- 2 files changed, 36 insertions(+), 59 deletions(-) delete mode 100644 lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs index afcfc4f95..97b8ce6ab 100644 --- a/lambda-events/src/event/vpc_lattice/v2.rs +++ b/lambda-events/src/event/vpc_lattice/v2.rs @@ -1,14 +1,13 @@ -use crate::custom_serde::{deserialize_headers, deserialize_nullish_boolean, http_method, serialize_headers}; +use crate::custom_serde::{deserialize_headers, deserialize_nullish_boolean, http_method, serialize_multi_value_headers}; use http::{HeaderMap, Method}; use query_map::QueryMap; use serde::{Deserialize, Serialize}; #[cfg(feature = "catch-all-fields")] use serde_json::Value; -// field ordering and types matched to example in -// https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#receive-event-from-service - /// `VpcLatticeRequestV2` contains data coming from VPC Lattice service (V2 format) +/// see: https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#receive-event-from-service +/// for field definitions. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct VpcLatticeRequestV2 { @@ -26,16 +25,11 @@ pub struct VpcLatticeRequestV2 { /// HTTP headers of the request (VPC Lattice V2 uses arrays for multi-values) #[serde(default, deserialize_with = "deserialize_headers")] - #[serde(serialize_with = "serialize_headers")] + #[serde(serialize_with = "serialize_multi_value_headers")] pub headers: HeaderMap, /// HTTP query string parameters (VPC Lattice V2 uses arrays for multi-values) - #[serde( - default, - deserialize_with = "query_map::serde::aws_api_gateway_v2::deserialize_empty" - )] - #[serde(skip_serializing_if = "QueryMap::is_empty")] - #[serde(serialize_with = "query_map::serde::aws_api_gateway_v2::serialize_query_string_parameters")] + #[serde(default)] pub query_string_parameters: QueryMap, /// The request body @@ -161,10 +155,12 @@ mod test { parsed.headers.get_all("user-agent").iter().nth(0).unwrap() ); + // headers including testing multi-values let header_multi = parsed.headers.get_all("multi"); assert_eq!("x", header_multi.iter().nth(0).unwrap()); assert_eq!("y", header_multi.iter().nth(1).unwrap()); + // query string including testing multi-values assert_eq!("prod", parsed.query_string_parameters.first("state").unwrap()); let query_multi = parsed.query_string_parameters.all("multi").unwrap(); assert_eq!(&"a", query_multi.iter().nth(0).unwrap()); @@ -174,6 +170,7 @@ mod test { assert!(parsed.body.is_none()); assert_eq!(false, parsed.is_base64_encoded); + // nested structure testing assert_eq!( "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", parsed.request_context.service_arn.unwrap() @@ -186,17 +183,41 @@ mod test { "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", parsed.request_context.target_group_arn.unwrap() ); + assert_eq!( + "ap-southeast-2", + parsed.request_context.region.unwrap() + ); + assert_eq!( + "1724875399456789", + parsed.request_context.time_epoch.unwrap() + ); + + let context = parsed.request_context.identity.as_ref().unwrap(); + + // identity assert_eq!( "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", - parsed.request_context.identity.unwrap().source_vpc_arn.unwrap() + context.clone().source_vpc_arn.unwrap() + ); + assert_eq!( + "AWS_IAM", + context.clone().identity_type.unwrap() + ); + assert_eq!( + "arn:aws:iam::123456789012:role/service-role/HealthChecker", + context.clone().principal.unwrap() + ); + assert_eq!( + "o-50dc6c495c0c9188", + context.clone().principal_org_id.unwrap() ); - //assert_eq!("", parsed.request_context.service_arn.unwrap()); - //assert_eq!("", parsed.request_context.service_arn.unwrap()); } #[test] #[cfg(feature = "vpc_lattice")] - fn example_vpc_lattice_v2_serde_round_trip_basic() { + fn example_vpc_lattice_v2_serde_round_trip() { + // our basic example has instances of multi-value headers and multi-value parameters + // so this test covers both those serialization edge cases let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); let output: String = serde_json::to_string(&parsed).unwrap(); @@ -214,16 +235,6 @@ mod test { assert_eq!(parsed, reparsed); } - #[test] - #[cfg(feature = "vpc_lattice")] - fn example_vpc_lattice_v2_serde_round_trip_multi_value_headers() { - let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-multi-value-headers.json"); - let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); - } - #[test] #[cfg(feature = "vpc_lattice")] fn example_vpc_lattice_v2_serde_round_trip_role_anywhere() { diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json deleted file mode 100644 index a7d4c04af..000000000 --- a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-multi-value-headers.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": "2.0", - "path": "/api/users/123/orders", - "method": "POST", - "headers": { - "content-type": ["application/json"], - "authorization": ["Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."], - "user-agent": ["MyApp/1.0"], - "x-forwarded-for": ["10.0.1.45"], - "x-custom-header": ["value1", "value2"], - "accept": ["application/json", "application/xml"] - }, - "queryStringParameters": { - "include": ["metadata", "shipping"], - "format": ["json"], - "debug": ["true"] - }, - "body": "{\"productId\": \"prod-456\", \"quantity\": 2, \"shippingAddress\": {\"street\": \"123 Main St\", \"city\": \"Melbourne\", \"state\": \"VIC\", \"postcode\": \"3000\"}}", - "isBase64Encoded": false, - "requestContext": { - "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", - "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", - "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", - "identity": { - "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", - "type": "AWS_IAM", - "principal": "arn:aws:iam::123456789012:assumed-role/OrderService-ExecutionRole/i-0c7de02a688bde9f7", - "principalOrgID": "o-50dc6c495c0c9188", - "sessionName": "i-0c7de02a688bde9f7" - }, - "region": "ap-southeast-2", - "timeEpoch": "1724875199177430" - } -} \ No newline at end of file From d8e1a08f57e5093241dc11a802756c093354523c Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Fri, 29 Aug 2025 11:46:16 +1000 Subject: [PATCH 6/8] nightly fmt --- lambda-events/src/event/vpc_lattice/mod.rs | 4 +--- .../serialization_comma_separated_headers.rs | 3 +-- lambda-events/src/event/vpc_lattice/v1.rs | 6 +++-- lambda-events/src/event/vpc_lattice/v2.rs | 24 ++++++------------- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/lambda-events/src/event/vpc_lattice/mod.rs b/lambda-events/src/event/vpc_lattice/mod.rs index c8668b5bc..211387d8b 100644 --- a/lambda-events/src/event/vpc_lattice/mod.rs +++ b/lambda-events/src/event/vpc_lattice/mod.rs @@ -4,9 +4,7 @@ mod v1; mod v2; // re-export types -pub use self::common::*; -pub use self::v1::*; -pub use self::v2::*; +pub use self::{common::*, v1::*, v2::*}; // helper code pub(crate) use self::serialization_comma_separated_headers::*; diff --git a/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs index f04c313f8..91922c411 100644 --- a/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs +++ b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs @@ -172,8 +172,7 @@ mod tests { use super::*; use http::{HeaderMap, HeaderValue}; use serde_json; - use serde_with::serde_derive::Deserialize; - use serde_with::serde_derive::Serialize; + use serde_with::serde_derive::{Deserialize, Serialize}; #[test] fn test_function_deserializer() { diff --git a/lambda-events/src/event/vpc_lattice/v1.rs b/lambda-events/src/event/vpc_lattice/v1.rs index 3125804d6..0196e2a14 100644 --- a/lambda-events/src/event/vpc_lattice/v1.rs +++ b/lambda-events/src/event/vpc_lattice/v1.rs @@ -4,8 +4,10 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "catch-all-fields")] use serde_json::Value; -use crate::custom_serde::{deserialize_nullish_boolean, http_method}; -use crate::vpc_lattice::{deserialize_comma_separated_headers, serialize_comma_separated_headers}; +use crate::{ + custom_serde::{deserialize_nullish_boolean, http_method}, + vpc_lattice::{deserialize_comma_separated_headers, serialize_comma_separated_headers}, +}; /// `VpcLatticeRequestV1` contains data coming from VPC Lattice service (V1 format) #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs index 97b8ce6ab..81dee3bfe 100644 --- a/lambda-events/src/event/vpc_lattice/v2.rs +++ b/lambda-events/src/event/vpc_lattice/v2.rs @@ -1,4 +1,6 @@ -use crate::custom_serde::{deserialize_headers, deserialize_nullish_boolean, http_method, serialize_multi_value_headers}; +use crate::custom_serde::{ + deserialize_headers, deserialize_nullish_boolean, http_method, serialize_multi_value_headers, +}; use http::{HeaderMap, Method}; use query_map::QueryMap; use serde::{Deserialize, Serialize}; @@ -183,14 +185,8 @@ mod test { "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", parsed.request_context.target_group_arn.unwrap() ); - assert_eq!( - "ap-southeast-2", - parsed.request_context.region.unwrap() - ); - assert_eq!( - "1724875399456789", - parsed.request_context.time_epoch.unwrap() - ); + assert_eq!("ap-southeast-2", parsed.request_context.region.unwrap()); + assert_eq!("1724875399456789", parsed.request_context.time_epoch.unwrap()); let context = parsed.request_context.identity.as_ref().unwrap(); @@ -199,18 +195,12 @@ mod test { "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", context.clone().source_vpc_arn.unwrap() ); - assert_eq!( - "AWS_IAM", - context.clone().identity_type.unwrap() - ); + assert_eq!("AWS_IAM", context.clone().identity_type.unwrap()); assert_eq!( "arn:aws:iam::123456789012:role/service-role/HealthChecker", context.clone().principal.unwrap() ); - assert_eq!( - "o-50dc6c495c0c9188", - context.clone().principal_org_id.unwrap() - ); + assert_eq!("o-50dc6c495c0c9188", context.clone().principal_org_id.unwrap()); } #[test] From 2ce4842fad513b36eb43501dae9e2d80e78c313c Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Fri, 29 Aug 2025 15:17:40 +1000 Subject: [PATCH 7/8] clippy fixes --- lambda-events/src/event/vpc_lattice/common.rs | 3 ++- .../serialization_comma_separated_headers.rs | 4 ++-- lambda-events/src/event/vpc_lattice/v1.rs | 6 +++--- lambda-events/src/event/vpc_lattice/v2.rs | 12 ++++++------ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lambda-events/src/event/vpc_lattice/common.rs b/lambda-events/src/event/vpc_lattice/common.rs index 2626ff18d..c14bf48d4 100644 --- a/lambda-events/src/event/vpc_lattice/common.rs +++ b/lambda-events/src/event/vpc_lattice/common.rs @@ -1,4 +1,5 @@ use crate::custom_serde::{deserialize_headers, serialize_headers}; +use crate::encodings::Body; use http::HeaderMap; use serde::{Deserialize, Serialize}; #[cfg(feature = "catch-all-fields")] @@ -30,7 +31,7 @@ pub struct VpcLatticeResponse { /// The response body #[serde(default)] - pub body: Option, + pub body: Option, /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. /// Enabled with Cargo feature `catch-all-fields`. diff --git a/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs index 91922c411..8ba0f41e2 100644 --- a/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs +++ b/lambda-events/src/event/vpc_lattice/serialization_comma_separated_headers.rs @@ -185,8 +185,8 @@ mod tests { let r: RequestWithHeaders = serde_json::from_str("{ \"headers\": {\"x-foo\": \"z\", \"x-multi\": \"abcd, DEF, w\" }}").unwrap(); - assert_eq!("z", r.headers.get_all("x-foo").iter().nth(0).unwrap()); - assert_eq!("abcd", r.headers.get_all("x-multi").iter().nth(0).unwrap()); + assert_eq!("z", r.headers.get_all("x-foo").iter().next().unwrap()); + assert_eq!("abcd", r.headers.get_all("x-multi").iter().next().unwrap()); assert_eq!("DEF", r.headers.get_all("x-multi").iter().nth(1).unwrap()); assert_eq!("w", r.headers.get_all("x-multi").iter().nth(2).unwrap()); } diff --git a/lambda-events/src/event/vpc_lattice/v1.rs b/lambda-events/src/event/vpc_lattice/v1.rs index 0196e2a14..7487e84ad 100644 --- a/lambda-events/src/event/vpc_lattice/v1.rs +++ b/lambda-events/src/event/vpc_lattice/v1.rs @@ -61,11 +61,11 @@ mod test { assert_eq!("POST", parsed.method); assert_eq!( "curl/7.68.0", - parsed.headers.get_all("user-agent").iter().nth(0).unwrap() + parsed.headers.get_all("user-agent").iter().next().unwrap() ); assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); assert_eq!("{\"id\": 5, \"description\": \"TV\"}", parsed.body.unwrap()); - assert_eq!(false, parsed.is_base64_encoded); + assert!(!parsed.is_base64_encoded); } #[test] @@ -74,7 +74,7 @@ mod test { let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); - assert_eq!("abcd", parsed.headers.get_all("multi").iter().nth(0).unwrap()); + assert_eq!("abcd", parsed.headers.get_all("multi").iter().next().unwrap()); assert_eq!("DEF", parsed.headers.get_all("multi").iter().nth(1).unwrap()); } diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs index 81dee3bfe..577900628 100644 --- a/lambda-events/src/event/vpc_lattice/v2.rs +++ b/lambda-events/src/event/vpc_lattice/v2.rs @@ -154,23 +154,23 @@ mod test { assert_eq!("GET", parsed.method); assert_eq!( "curl/7.68.0", - parsed.headers.get_all("user-agent").iter().nth(0).unwrap() + parsed.headers.get_all("user-agent").iter().next().unwrap() ); // headers including testing multi-values let header_multi = parsed.headers.get_all("multi"); - assert_eq!("x", header_multi.iter().nth(0).unwrap()); + assert_eq!("x", header_multi.iter().next().unwrap()); assert_eq!("y", header_multi.iter().nth(1).unwrap()); // query string including testing multi-values assert_eq!("prod", parsed.query_string_parameters.first("state").unwrap()); let query_multi = parsed.query_string_parameters.all("multi").unwrap(); - assert_eq!(&"a", query_multi.iter().nth(0).unwrap()); - assert_eq!(&"DEF", query_multi.iter().nth(1).unwrap()); - assert_eq!(&"g", query_multi.iter().nth(2).unwrap()); + assert_eq!(&"a", query_multi.first().unwrap()); + assert_eq!(&"DEF", query_multi.get(1).unwrap()); + assert_eq!(&"g", query_multi.get(2).unwrap()); assert!(parsed.body.is_none()); - assert_eq!(false, parsed.is_base64_encoded); + assert!(!parsed.is_base64_encoded); // nested structure testing assert_eq!( From ef22e5853ac45c017c8d473278002389ac0383f8 Mon Sep 17 00:00:00 2001 From: andrewpatto Date: Fri, 29 Aug 2025 15:29:49 +1000 Subject: [PATCH 8/8] add basic response test --- lambda-events/src/event/vpc_lattice/common.rs | 15 +++++++++++++++ ...nse.json => example-vpc-lattice-response.json} | 0 2 files changed, 15 insertions(+) rename lambda-events/src/fixtures/{example-vpc-lattice-v2-response.json => example-vpc-lattice-response.json} (100%) diff --git a/lambda-events/src/event/vpc_lattice/common.rs b/lambda-events/src/event/vpc_lattice/common.rs index c14bf48d4..724a01b2c 100644 --- a/lambda-events/src/event/vpc_lattice/common.rs +++ b/lambda-events/src/event/vpc_lattice/common.rs @@ -41,3 +41,18 @@ pub struct VpcLatticeResponse { #[serde(flatten)] pub other: serde_json::Map, } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_alb_lambda_target_response() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-response.json"); + let parsed: VpcLatticeResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-response.json b/lambda-events/src/fixtures/example-vpc-lattice-response.json similarity index 100% rename from lambda-events/src/fixtures/example-vpc-lattice-v2-response.json rename to lambda-events/src/fixtures/example-vpc-lattice-response.json