From f7f64e336155276c741663bafc94d033b0cb16eb Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Thu, 21 Mar 2019 22:18:34 +0100 Subject: [PATCH] Also parse `serialize_always` --- CHANGELOG.md | 2 + serde_with_macros/src/lib.rs | 109 ++++++++++++------ .../tests/skip_serializing_null.rs | 35 +++++- 3 files changed, 108 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9373ddbd..14b6127d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * Add `skip_serializing_null` attribute, which adds `#[serde(skip_serializing_if = "Option::is_none")]` for each Option in a struct. This is helpfull for APIs which have many optional fields. + The effect of can be negated by adding `serialize_always` on those fields, which should always be serialized. + Existing `skip_serializing_if` will never be modified and those fields keep their behavior. ## [1.2.0] diff --git a/serde_with_macros/src/lib.rs b/serde_with_macros/src/lib.rs index 235c4043..73b920a5 100644 --- a/serde_with_macros/src/lib.rs +++ b/serde_with_macros/src/lib.rs @@ -24,6 +24,36 @@ use syn::{ Type, }; +#[proc_macro_attribute] +pub fn skip_serializing_null(_args: TokenStream, input: TokenStream) -> TokenStream { + let res = match skip_serializing_null_do(input) { + Ok(res) => res, + Err(msg) => { + let span = Span::call_site(); + Error::new(span, msg).to_compile_error() + } + }; + TokenStream::from(res) +} + +fn skip_serializing_null_do(input: TokenStream) -> Result { + // For each field in the struct given by `input`, add the `skip_serializing_if` attribute, + // if and only if, it is of type `Option` + if let Ok(mut input) = syn::parse::(input.clone()) { + skip_serializing_null_handle_fields(&mut input.fields)?; + Ok(quote!(#input)) + } else if let Ok(mut input) = syn::parse::(input.clone()) { + input + .variants + .iter_mut() + .map(|variant| skip_serializing_null_handle_fields(&mut variant.fields)) + .collect::>()?; + Ok(quote!(#input)) + } else { + Err("The attribute can only be applied to struct or enum definitions.".into()) + } +} + /// Return `true`, if the type path refers to `std::option::Option` /// /// Accepts @@ -78,16 +108,40 @@ fn field_has_attribute(field: &Field, namespace: &str, name: &str) -> bool { } /// Add the skip_serializing_if annotation to each field of the struct -fn skip_serializing_null_add_attr_to_field<'a>(fields: impl IntoIterator) { - fields.into_iter().for_each(|field| { - if let Type::Path(path) = &field.ty { +fn skip_serializing_null_add_attr_to_field<'a>( + fields: impl IntoIterator, +) -> Result<(), String> { + fields.into_iter().map(|field| ->Result<(), String> { + if let Type::Path(path) = &field.ty.clone() { if is_std_option(&path.path) { - // Do nothing, if the value already exists - if field_has_attribute(&field, "serde", "skip_serializing_if") { - return; + let has_skip_serializing_if = + field_has_attribute(&field, "serde", "skip_serializing_if"); + + // Remove the `serialize_always` attribute + let mut has_always_attr = false; + field.attrs.retain(|attr| { + let has_attr = attr.path.is_ident("serialize_always"); + has_always_attr |= has_attr; + !has_attr + }); + + // Error on conflicting attributes + if has_always_attr && has_skip_serializing_if { + let mut msg = r#"The attributes `serialize_always` and `serde(skip_serializing_if = "...")` cannot be used on the same field"#.to_string(); + if let Some(ident) = &field.ident { + msg += ": `"; + msg += &ident.to_string(); + msg += "`"; + } + msg +="."; + return Err(msg); + } + + // Do nothing if `skip_serializing_if` or `serialize_always` is already present + if has_skip_serializing_if || has_always_attr { + return Ok(()); } - // FIXME if skip_serializing_if already exists, do not add it again // Add the `skip_serializing_if` attribute let attr_tokens = quote!( #[serde(skip_serializing_if = "Option::is_none")] @@ -97,45 +151,30 @@ fn skip_serializing_null_add_attr_to_field<'a>(fields: impl IntoIterator Result<(), String> { match fields { // simple, no fields, do nothing - Fields::Unit => {} + Fields::Unit => Ok(()), Fields::Named(ref mut fields) => { skip_serializing_null_add_attr_to_field(fields.named.iter_mut()) } Fields::Unnamed(ref mut fields) => { skip_serializing_null_add_attr_to_field(fields.unnamed.iter_mut()) } - }; -} - -#[proc_macro_attribute] -pub fn skip_serializing_null(_args: TokenStream, input: TokenStream) -> TokenStream { - // For each field in the struct given by `input`, add the `skip_serializing_if` attribute, - // if and only if, it is of type `Option` - let res = if let Ok(mut input) = syn::parse::(input.clone()) { - skip_serializing_null_handle_fields(&mut input.fields); - quote!(#input) - } else if let Ok(mut input) = syn::parse::(input.clone()) { - input.variants.iter_mut().for_each(|variant| { - skip_serializing_null_handle_fields(&mut variant.fields); - }); - quote!(#input) - } else { - let span = Span::call_site(); - Error::new( - span, - "The attribute can only be applied to struct or enum definitions.", - ) - .to_compile_error() - }; - // Hand the modified input back to the compiler - TokenStream::from(res) + } } diff --git a/serde_with_macros/tests/skip_serializing_null.rs b/serde_with_macros/tests/skip_serializing_null.rs index 34217a4f..7fe1595c 100644 --- a/serde_with_macros/tests/skip_serializing_null.rs +++ b/serde_with_macros/tests/skip_serializing_null.rs @@ -78,9 +78,7 @@ struct DataExistingAnnotation { #[test] fn test_existing_annotation() { - let expected = json!({ - "name": null - }); + let expected = json!({ "name": null }); let data = DataExistingAnnotation { a: None, b: None, @@ -92,6 +90,37 @@ fn test_existing_annotation() { assert_eq!(data, serde_json::from_value(res).unwrap()); } +#[skip_serializing_null] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +struct DataSerializeAlways { + #[serialize_always] + a: Option, + #[serialize_always] + b: Option, + c: i64, + #[serialize_always] + d: Option, +} + +#[test] +fn test_serialize_always() { + let expected = json!({ + "a": null, + "b": null, + "c": 0, + "d": null + }); + let data = DataSerializeAlways { + a: None, + b: None, + c: 0, + d: None, + }; + let res = serde_json::to_value(&data).unwrap(); + assert_eq!(expected, res); + assert_eq!(data, serde_json::from_value(res).unwrap()); +} + #[skip_serializing_null] #[derive(Debug, Eq, PartialEq, Serialize)] struct DataTuple(Option, std::option::Option);