-
Notifications
You must be signed in to change notification settings - Fork 388
fix handling of null groupConfiguration #1130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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()) | ||
| } | ||
|
|
@@ -107,7 +77,9 @@ where | |
| #[allow(deprecated)] | ||
| mod test { | ||
| use super::*; | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
| use std::collections::HashMap; | ||
|
|
||
| #[test] | ||
| fn test_deserialize_base64() { | ||
|
|
@@ -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!({ | ||
|
|
@@ -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!({ | ||
|
|
@@ -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:?}") | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. none of the existing test cases demonstrated behavior on a missing field, so added that here. |
||
| } | ||
|
|
||
| #[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, | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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))] | ||
|
|
@@ -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")] | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this one was used a lot. i do think it slightly improves the documentation in the code, since "deserialize_lambda_map" tells you something you already know about the field, whereas "deserialize_nullish" suggests what it's doing. |
||
| #[serde(default)] | ||
| pub properties: HashMap<String, String>, | ||
| /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems odd that this is the first we've encountered this. Would this helper be useful elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see, this is a hack to work around us not setting the field as
Option<T>in the first place. Seems fine. Maybe add that context in the doc comment (prefer using Option<T>, this is for...)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, I don't mind scope creeping those in if you feel like it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow thanks for the turnaround. i was not expecting a same-day review for a PR.
will do. i looked at the history before starting and i think the gap is really in the cognito doc. they should publish json schema or similar, anything more contractual than just sample JSON. it's not clear that the field should be optional because it's in every provided example.
in short, yes. mentioned in the commit comment, it's nearly identical to several existing functions in the same file and generalizes them (nullish_boolean, lambda_map, and dynamodb_item). i think you could probably remove all of those and just use this one instead.
however that change has a bigger blast radius, and I wasn't in a position to integration test it thoroughly myself. it's not hard though, i can put that in a separate commit on my fork if you want to eyeball it.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For cognito docs, there is a "give feedback" button somewhere on the page. It would be great to give the technical writers with cognito a heads up about the gap.
Either way, we would rather stay defensive against this schema issue and avoid failing the overall deserialization.
Glad to review a PR to migrate others - if we have unit tests guarding that the behavior is unchanged (we should, with fixtures, just make sure we have fixtures exercising the null handling that pass against both approaches), i see no harm in cutting to the helper. No pressure, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dropped some feedback on the doc page per your suggestion. I included a pointer back to the issue report here for motivation.