Skip to content
Merged
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
84 changes: 41 additions & 43 deletions lambda-events/src/custom_serde/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use serde::{
de::{Deserialize, Deserializer, Error as DeError},
ser::Serializer,
};
use std::collections::HashMap;

#[cfg(feature = "codebuild")]
pub(crate) mod codebuild_time;
Expand Down Expand Up @@ -58,47 +57,18 @@ where
serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(value))
}

/// Deserializes `HashMap<_>`, mapping JSON `null` to an empty map.
pub(crate) fn deserialize_lambda_map<'de, D, K, V>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
/// Deserializes any `Default` type, mapping JSON `null` to `T::default()`.
///
/// **Note** null-to-empty semantics are usually clear for container types (Map, Vec, etc).
/// For most other data types, prefer modeling fields as ```Option<T>``` with #[serde(default)]
/// instead of using this deserializer. Option preserves information about the message
/// for the application, and default semantics for the target data type may change
/// over time without warning.
pub(crate) fn deserialize_nullish<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
K: serde::Deserialize<'de>,
K: std::hash::Hash,
K: std::cmp::Eq,
V: serde::Deserialize<'de>,
T: Default + Deserialize<'de>,
{
// https://github.com/serde-rs/serde/issues/1098
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}

#[cfg(feature = "dynamodb")]
/// Deserializes `Item`, mapping JSON `null` to an empty item.
pub(crate) fn deserialize_lambda_dynamodb_item<'de, D>(deserializer: D) -> Result<serde_dynamo::Item, D::Error>
where
D: Deserializer<'de>,
{
// https://github.com/serde-rs/serde/issues/1098
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}

/// Deserializes `HashMap<_>`, mapping JSON `null` to an empty map.
#[cfg(any(
feature = "alb",
feature = "apigw",
feature = "cloudwatch_events",
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>
where
D: Deserializer<'de>,
{
// https://github.com/serde-rs/serde/issues/1098
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
Expand All @@ -107,7 +77,9 @@ where
#[allow(deprecated)]
mod test {
use super::*;

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[test]
fn test_deserialize_base64() {
Expand Down Expand Up @@ -141,7 +113,7 @@ mod test {
fn test_deserialize_map() {
#[derive(Deserialize)]
struct Test {
#[serde(deserialize_with = "deserialize_lambda_map")]
#[serde(deserialize_with = "deserialize_nullish")]
v: HashMap<String, String>,
}
let input = serde_json::json!({
Expand All @@ -160,9 +132,9 @@ mod test {
#[cfg(feature = "dynamodb")]
#[test]
fn test_deserialize_lambda_dynamodb_item() {
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
struct Test {
#[serde(deserialize_with = "deserialize_lambda_dynamodb_item")]
#[serde(deserialize_with = "deserialize_nullish")]
v: serde_dynamo::Item,
}
let input = serde_json::json!({
Expand All @@ -176,13 +148,39 @@ mod test {
});
let decoded: Test = serde_json::from_value(input).unwrap();
assert_eq!(serde_dynamo::Item::from(HashMap::new()), decoded.v);

let input = serde_json::json!({});
let failure = serde_json::from_value::<Test>(input);
assert!(failure.is_err(), "Missing field should not default: {failure:?}")
}

#[test]
fn test_deserialize_nullish() {
#[derive(Debug, Default, Deserialize, PartialEq)]
struct Inner {
x: u32,
}
#[derive(Deserialize)]
struct Test {
#[serde(default, deserialize_with = "deserialize_nullish")]
v: Inner,
}

let decoded: Test = serde_json::from_str(r#"{"v": null}"#).unwrap();
assert_eq!(decoded.v, Inner::default());

let decoded: Test = serde_json::from_str(r#"{}"#).unwrap();
assert_eq!(decoded.v, Inner::default());

let decoded: Test = serde_json::from_str(r#"{"v": {"x": 42}}"#).unwrap();
assert_eq!(decoded.v, Inner { x: 42 });
}

#[test]
fn test_deserialize_nullish_boolean() {
#[derive(Deserialize)]
struct Test {
#[serde(default, deserialize_with = "deserialize_nullish_boolean")]
#[serde(default, deserialize_with = "deserialize_nullish")]
v: bool,
}

Expand Down
24 changes: 17 additions & 7 deletions lambda-events/src/event/activemq/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;

use crate::custom_serde::deserialize_lambda_map;
use crate::custom_serde::deserialize_nullish;

#[non_exhaustive]
#[cfg_attr(feature = "builders", derive(Builder))]
Expand Down Expand Up @@ -54,7 +54,7 @@ pub struct ActiveMqMessage {
pub data: Option<String>,
pub broker_in_time: i64,
pub broker_out_time: i64,
#[serde(deserialize_with = "deserialize_lambda_map")]
#[serde(deserialize_with = "deserialize_nullish")]
#[serde(default)]
pub properties: HashMap<String, String>,
/// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
Expand Down Expand Up @@ -87,14 +87,24 @@ pub struct ActiveMqDestination {
#[cfg(test)]
mod test {
use super::*;
use crate::fixtures::verify_serde_roundtrip;

#[test]
#[cfg(feature = "activemq")]
fn example_activemq_event() {
let data = include_bytes!("../../fixtures/example-activemq-event.json");
let parsed: ActiveMqEvent = serde_json::from_slice(data).unwrap();
let output: String = serde_json::to_string(&parsed).unwrap();
let reparsed: ActiveMqEvent = serde_json::from_slice(output.as_bytes()).unwrap();
assert_eq!(parsed, reparsed);
verify_serde_roundtrip::<ActiveMqEvent>(include_bytes!("../../fixtures/example-activemq-event.json"));
}

#[test]
#[cfg(feature = "activemq")]
fn example_activemq_event_null_properties() {
let event: ActiveMqEvent = verify_serde_roundtrip(include_bytes!(
"../../fixtures/example-activemq-event-null-properties.json"
));
assert_eq!(
0,
event.messages[0].properties.len(),
"null properties should deserialize to empty map"
)
}
}
8 changes: 4 additions & 4 deletions lambda-events/src/event/alb/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
custom_serde::{
deserialize_headers, deserialize_nullish_boolean, http_method, serialize_headers,
serialize_multi_value_headers, serialize_query_string_parameters,
deserialize_headers, deserialize_nullish, http_method, serialize_headers, serialize_multi_value_headers,
serialize_query_string_parameters,
},
encodings::Body,
};
Expand Down Expand Up @@ -35,7 +35,7 @@ pub struct AlbTargetGroupRequest {
#[serde(serialize_with = "serialize_multi_value_headers")]
pub multi_value_headers: HeaderMap,
pub request_context: AlbTargetGroupRequestContext,
#[serde(default, deserialize_with = "deserialize_nullish_boolean")]
#[serde(default, deserialize_with = "deserialize_nullish")]
pub is_base64_encoded: bool,
pub body: Option<String>,
/// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
Expand Down Expand Up @@ -101,7 +101,7 @@ pub struct AlbTargetGroupResponse {
pub multi_value_headers: HeaderMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<Body>,
#[serde(default, deserialize_with = "deserialize_nullish_boolean")]
#[serde(default, deserialize_with = "deserialize_nullish")]
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`.
Expand Down
Loading
Loading