Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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
Expand Down
2 changes: 2 additions & 0 deletions lambda-events/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ default = [
"streams",
"documentdb",
"eventbridge",
"vpc_lattice"
]

activemq = []
Expand Down Expand Up @@ -125,6 +126,7 @@ sqs = ["serde_with"]
streams = []
documentdb = []
eventbridge = ["chrono", "serde_with"]
vpc_lattice = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"]

catch-all-fields = []
builders = ["bon"]
Expand Down
83 changes: 74 additions & 9 deletions lambda-events/src/custom_serde/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ where
map.end()
}

/// Serialize a HeaderMap with multiple values per header combined as comma-separated strings.
/// Used by VPC Lattice V1 which expects multi-value headers as "value1, value2".
#[cfg(feature = "vpc_lattice")]
pub(crate) fn serialize_comma_separated_headers<S>(headers: &HeaderMap, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(headers.keys_len()))?;
for key in headers.keys() {
let values: Vec<&str> = headers.get_all(key).iter().filter_map(|v| v.to_str().ok()).collect();
if !values.is_empty() {
let combined_value = values.join(", ");
map.serialize_entry(key.as_str(), &combined_value)?;
}
}
map.end()
}

#[derive(serde::Deserialize)]
#[serde(untagged)]
enum OneOrMore<'a> {
Expand All @@ -44,6 +62,7 @@ enum OneOrMore<'a> {

struct HeaderMapVisitor {
is_human_readable: bool,
split_comma_separated: bool,
}

impl<'de> Visitor<'de> for HeaderMapVisitor {
Expand Down Expand Up @@ -97,17 +116,25 @@ impl<'de> Visitor<'de> for HeaderMapVisitor {
.map_err(|_| de::Error::invalid_value(Unexpected::Str(&key), &self))?;
match val {
OneOrMore::One(val) => {
let val = val
.parse()
.map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?;
map.insert(key, val);
}
OneOrMore::Strings(arr) => {
for val in arr {
if self.split_comma_separated && val.contains(',') {
split_and_append_header(&mut map, &key, &val, &self)?;
} else {
let val = val
.parse()
.map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?;
map.append(&key, val);
map.insert(key, val);
}
}
OneOrMore::Strings(arr) => {
for val in arr {
if self.split_comma_separated && val.contains(',') {
split_and_append_header(&mut map, &key, &val, &self)?;
} else {
let val = val
.parse()
.map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?;
map.append(&key, val);
}
}
}
OneOrMore::Bytes(arr) => {
Expand All @@ -124,13 +151,51 @@ impl<'de> Visitor<'de> for HeaderMapVisitor {
}
}

fn split_and_append_header<E>(
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() {
let header_val = trimmed_val
.parse()
.map_err(|_| de::Error::invalid_value(Unexpected::Str(trimmed_val), visitor))?;
map.append(key, header_val);
}
}
Ok(())
}

/// Implementation detail.
pub(crate) fn deserialize_headers<'de, D>(de: D) -> Result<HeaderMap, D::Error>
where
D: Deserializer<'de>,
{
let is_human_readable = de.is_human_readable();
de.deserialize_option(HeaderMapVisitor { is_human_readable })
de.deserialize_option(HeaderMapVisitor {
is_human_readable,
split_comma_separated: false,
})
}

/// Deserialize headers, splitting comma-separated values into multiple header values.
/// Used by VPC Lattice V1 which sends multi-value headers as "value1, value2".
#[cfg(feature = "vpc_lattice")]
pub(crate) fn deserialize_comma_separated_headers<'de, D>(de: D) -> Result<HeaderMap, D::Error>
where
D: Deserializer<'de>,
{
let is_human_readable = de.is_human_readable();
de.deserialize_option(HeaderMapVisitor {
is_human_readable,
split_comma_separated: true,
})
}

#[cfg(test)]
Expand Down
9 changes: 6 additions & 3 deletions lambda-events/src/custom_serde/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@ pub type CodeBuildNumber = f32;
feature = "apigw",
feature = "s3",
feature = "iot",
feature = "lambda_function_urls"
feature = "lambda_function_urls",
feature = "vpc_lattice"
))]
mod headers;
#[cfg(any(
feature = "alb",
feature = "apigw",
feature = "s3",
feature = "iot",
feature = "lambda_function_urls"
feature = "lambda_function_urls",
feature = "vpc_lattice"
))]
pub(crate) use self::headers::*;

#[cfg(feature = "dynamodb")]
pub(crate) mod float_unix_epoch;

#[cfg(any(feature = "alb", feature = "apigw"))]
#[cfg(any(feature = "alb", feature = "apigw", feature = "vpc_lattice"))]
pub(crate) mod http_method;

#[cfg(feature = "alb")]
Expand Down Expand Up @@ -89,6 +91,7 @@ where
feature = "code_commit",
feature = "cognito",
feature = "sns",
feature = "vpc_lattice",
test
))]
pub(crate) fn deserialize_nullish_boolean<'de, D>(deserializer: D) -> Result<bool, D::Error>
Expand Down
5 changes: 5 additions & 0 deletions lambda-events/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
62 changes: 62 additions & 0 deletions lambda-events/src/event/vpc_lattice/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use crate::{
custom_serde::{deserialize_headers, serialize_headers},
encodings::Body,
};
use http::HeaderMap;

#[cfg(feature = "builders")]
use bon::Builder;
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
#[non_exhaustive]
#[cfg_attr(feature = "builders", derive(Builder))]
#[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 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<Body>,

/// 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)]
#[cfg_attr(feature = "builders", builder(default))]
pub other: serde_json::Map<String, Value>,
}

#[cfg(test)]
mod test {
use super::*;

#[test]
#[cfg(feature = "vpc_lattice")]
fn example_vpc_lattice_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);
}
}
20 changes: 20 additions & 0 deletions lambda-events/src/event/vpc_lattice/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};

mod common;
mod v1;
mod v2;

// re-export types
pub use self::{common::*, v1::*, v2::*};

/// A VPC Lattice request that can be either V1 or V2 format.
/// Deserialization tries V2 first (which has a `version` and `requestContext` field),
/// then falls back to V1.
#[non_exhaustive]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum VpcLatticeRequest {
V2(VpcLatticeRequestV2),
V1(VpcLatticeRequestV1),
}
100 changes: 100 additions & 0 deletions lambda-events/src/event/vpc_lattice/v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#[cfg(feature = "builders")]
use bon::Builder;
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_comma_separated_headers, deserialize_nullish_boolean, http_method,
serialize_comma_separated_headers,
},
encodings::Body,
};

/// `VpcLatticeRequestV1` contains data coming from VPC Lattice service (V1 format)
#[non_exhaustive]
#[cfg_attr(feature = "builders", derive(Builder))]
#[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
pub raw_path: String,

/// 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<Body>,

/// 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)]
#[cfg_attr(feature = "builders", builder(default))]
pub other: serde_json::Map<String, Value>,
}

#[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);
assert_eq!("POST", parsed.method);
assert_eq!(
"curl/7.68.0",
parsed.headers.get_all("user-agent").iter().next().unwrap()
);
assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap());
assert_eq!(
Body::Text("{\"id\": 5, \"description\": \"TV\"}".to_string()),
parsed.body.unwrap()
);
assert!(!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().next().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());
}
}
Loading
Loading