Skip to content

Commit

Permalink
Also parse serialize_always
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbb committed Mar 31, 2019
1 parent ffcaf00 commit f7f64e3
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 38 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
109 changes: 74 additions & 35 deletions serde_with_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<proc_macro2::TokenStream, String> {
// 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::<ItemStruct>(input.clone()) {
skip_serializing_null_handle_fields(&mut input.fields)?;
Ok(quote!(#input))
} else if let Ok(mut input) = syn::parse::<ItemEnum>(input.clone()) {
input
.variants
.iter_mut()
.map(|variant| skip_serializing_null_handle_fields(&mut variant.fields))
.collect::<Result<(), _>>()?;
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
Expand Down Expand Up @@ -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<Item = &'a mut Field>) {
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<Item = &'a mut Field>,
) -> 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")]
Expand All @@ -97,45 +151,30 @@ fn skip_serializing_null_add_attr_to_field<'a>(fields: impl IntoIterator<Item =
.parse2(attr_tokens)
.expect("Static attr tokens should not panic");
field.attrs.extend(attrs);
} else {
// Warn on use of `serialize_always` on non-Option fields
let has_attr= field.attrs.iter().any(|attr| {
attr.path.is_ident("serialize_always")
});
if has_attr {
return Err("`serialize_always` may only be used on fields of type `Option`.".into());
}
}
}
})
Ok(())
}).collect()
}

/// Handle a single struct or a single enum variant
fn skip_serializing_null_handle_fields(fields: &mut Fields) {
fn skip_serializing_null_handle_fields(fields: &mut Fields) -> 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::<ItemStruct>(input.clone()) {
skip_serializing_null_handle_fields(&mut input.fields);
quote!(#input)
} else if let Ok(mut input) = syn::parse::<ItemEnum>(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)
}
}
35 changes: 32 additions & 3 deletions serde_with_macros/tests/skip_serializing_null.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<String>,
#[serialize_always]
b: Option<String>,
c: i64,
#[serialize_always]
d: Option<String>,
}

#[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<String>, std::option::Option<String>);
Expand Down

0 comments on commit f7f64e3

Please sign in to comment.