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 @@ -78,6 +78,7 @@ default = [
"streams",
"documentdb",
"eventbridge",
"vpc_lattice"
]

activemq = []
Expand Down Expand Up @@ -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 = []

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;
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]
#[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
// i64 for consistency with other event types (e.g. AlbTargetGroupResponse, ApiGatewayProxyResponse)
pub status_code: i64,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: oof it's a bit odd to use 64 bits for a http status code, I would have gone with u16. I guess it is a breaking change for the others, but maybe new events should use more efficient sizing?


/// The HTTP status description (optional)
#[serde(default)]
pub status_description: Option<String>,

/// 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)]
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);
}
}
6 changes: 6 additions & 0 deletions lambda-events/src/event/vpc_lattice/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod common;
mod v1;
mod v2;

// re-export types
pub use self::{common::*, v1::*, v2::*};
90 changes: 90 additions & 0 deletions lambda-events/src/event/vpc_lattice/v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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,
};

/// `VpcLatticeRequestV1` contains data coming from VPC Lattice service (V1 format)
#[non_exhaustive]
#[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<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<String>,

/// 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<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.unwrap());
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!("{\"id\": 5, \"description\": \"TV\"}", 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