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 @@ -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
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
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;
58 changes: 58 additions & 0 deletions lambda-events/src/event/vpc_lattice/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::custom_serde::{deserialize_headers, serialize_headers};
use crate::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
#[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<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_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);
}
}
10 changes: 10 additions & 0 deletions lambda-events/src/event/vpc_lattice/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mod common;
mod serialization_comma_separated_headers;
mod v1;
mod v2;

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

// helper code
pub(crate) use self::serialization_comma_separated_headers::*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
use http::{header::HeaderName, HeaderMap, HeaderValue};
use serde::{
de::{self, Deserializer, Error as DeError, MapAccess, Unexpected, Visitor},
ser::{SerializeMap, Serializer},
};
use std::{borrow::Cow, fmt};

/// Deserialize (potentially) comma separated headers into a HeaderMap
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 })
}

/// Serialize a HeaderMap with multiple values per header combined as comma-separated strings
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()))?;

// 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> {
One(Cow<'a, str>),
Strings(Vec<Cow<'a, str>>),
Bytes(Vec<Cow<'a, [u8]>>),
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_none<E>(self) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(HeaderMap::default())
}

fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(self)
}

fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: DeError,
{
Ok(HeaderMap::default())
}

fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
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::<Cow<'_, str>, Vec<Cow<'_, [u8]>>>()? {
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::<Cow<'_, str>, 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<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() {
// 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(())
}

#[cfg(test)]
mod tests {
use super::*;
use http::{HeaderMap, HeaderValue};
use serde_json;
use serde_with::serde_derive::{Deserialize, Serialize};

#[test]
fn test_function_deserializer() {
#[derive(Deserialize)]
struct RequestWithHeaders {
#[serde(deserialize_with = "deserialize_comma_separated_headers")]
headers: HeaderMap,
}

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().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());
}

fn create_test_headermap() -> HeaderMap {
let mut headers = HeaderMap::new();

// Single value header
headers.insert("content-type", HeaderValue::from_static("application/json"));

// 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
}

#[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(", "));
}
}
Loading