Skip to content
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

Create a proc-macro attribute which applies a second attribute on all fields of a type. #85

Closed
jonasbb opened this issue Feb 21, 2020 · 4 comments
Labels
enhancement New feature or request

Comments

@jonasbb
Copy link
Owner

jonasbb commented Feb 21, 2020

Serialization code is often repetitive. One helper to improve on this already exists in this crate, the serde_with::skip_serializing_none attribute.

However, this could be generalized to work with any attribute on a user-specified type (list).
This could look like

#[apply(Vec, HashMap => #[serde(skip_serializing_if = "$TYPE::is_empty"])]
struct {}

This would apply the attribute on all fields of type Vec and HashMap. On the right hand side, the placeholder $TYPE would be replaced by the type of the field. This would make it easier to skip serializing all empty container types.

serde_with::skip_serializing_none could in the end be implemented on top of this more general proc-macro.

@jonasbb jonasbb added the enhancement New feature or request label Aug 20, 2020
@xmo-odoo
Copy link

xmo-odoo commented May 30, 2022

A supplemental use-case for this is a companion to skip_serializing_none: APIs which need that (e.g. JSON swagger / openapi) tend to need skip_serializing_if = "Option::is_none" on the serialization side, but also default on the deserialization side.

skip_serializing_none only covers the first one, so when one needs to handle both sides of the serialization (sometimes on the same struct e.g. because it's defined for both the client and server to use) it can be a bit frustrating.

Possible issues with this:

  • does serde allow multple #[serde] annotations per field? What if a user needs both a blanket annotation, and a specific annotation on a specific field (e.g. rename)?
  • what about reverting the behaviour for one specific field? e.g. if the user wants to bulk-apply "skip empty collections" but one or two of them must be serialised empty? Or an API which mixes optional fields (skipped) and required nullable fields (not skipped)?

I guess the macro could just bail (refuse to compile) if it sees a #[serde] annotation on a field it tries to configure, seems safer than trying to mess around even if it means the user is back to hand-rolling the entire thing.

@jonasbb
Copy link
Owner Author

jonasbb commented May 30, 2022

A supplemental use-case for this is a companion to skip_serializing_none: APIs which need that (e.g. JSON swagger / openapi) tend to need skip_serializing_if = "Option::is_none" on the serialization side, but also default on the deserialization side.

You do not need default on a field of type Option. Missing fields automatically default to None. The exception is if you use #[serde(with = ...)] or #[serde_as(...)]. In the first case using default changes the meaning, and the second case is tracked here #185.

  • does serde allow multple #[serde] annotations per field? What if a user needs both a blanket annotation, and a specific annotation on a specific field (e.g. rename)?

Multiple annotations work flawlessly. Their individual meanings get joined.

  • what about reverting the behaviour for one specific field? e.g. if the user wants to bulk-apply "skip empty collections" but one or two of them must be serialised empty? Or an API which mixes optional fields (skipped) and required nullable fields (not skipped)?

Similar to skip_serializing_none a simple #[no_apply] probably makes sense. It should mark the field to be ignored for any apply macro. Having two #[apply] apply on one type is probably rare, and even rarer that they overlap but you only want to opt-out of one apply. In such corner cases, all attributes can always be applied manually on the fields.

I guess the macro could just bail (refuse to compile) if it sees a #[serde] annotation on a field it tries to configure, seems safer than trying to mess around even if it means the user is back to hand-rolling the entire thing.

I would not want the apply macro to parse any annotations not meant for it. This means if there is already a #[serde] on the field, that shouldn't bother the apply macro. If any other macros or derives cannot handle that, then that is a user/other crate problem. The reason here is simply that the annotation to be applied on the field does not have to be serde related. It could also be for any other derive. So erroring, if there is a #[serde] annotation, is wrong.

@xmo-odoo
Copy link

A supplemental use-case for this is a companion to skip_serializing_none: APIs which need that (e.g. JSON swagger / openapi) tend to need skip_serializing_if = "Option::is_none" on the serialization side, but also default on the deserialization side.

You do not need default on a field of type Option. Missing fields automatically default to None.

Well now I'm astonished, I was convinced it'd yield a parsing error.

That is rather convenient, thank you very much.

jonasbb added a commit that referenced this issue Nov 6, 2022
This `apply` proc-macro takes a list of type patterns and a list of
attributes for each. On each field where the type matches, the list of
attributes will be added to the existing field attributes.
bors bot added a commit that referenced this issue Nov 13, 2022
524: Start work on an `apply` attribute #85 r=jonasbb a=jonasbb

This `apply` proc-macro takes a list of type patterns and a list of attributes for each. On each field where the type matches, the list of attributes will be added to the existing field attributes.

```rust
#[serde_with::apply(
    Option => #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")],
    Option<bool> => #[serde(rename = "bool")],
)]
#[derive(serde::Serialize)]
struct Data {
    a: Option<String>,
    b: Option<u64>,
    c: Option<String>,
    d: Option<bool>,
}
```

The `apply` attribute will expand into this, applying the attributs to the matching fields:

```rust
#[derive(serde::Serialize)]
struct Data {
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    a: Option<String>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    b: Option<u64>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    c: Option<String>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "bool")]
    d: Option<bool>,
}
```

bors r+

Co-authored-by: Jonas Bushart <jonas@bushart.org>
jonasbb added a commit that referenced this issue Nov 13, 2022
This `apply` proc-macro takes a list of type patterns and a list of
attributes for each. On each field where the type matches, the list of
attributes will be added to the existing field attributes.
bors bot added a commit that referenced this issue Nov 13, 2022
524: Implement `apply` attribute #85 r=jonasbb a=jonasbb

This `apply` proc-macro takes a list of type patterns and a list of attributes for each. On each field where the type matches, the list of attributes will be added to the existing field attributes.

```rust
#[serde_with::apply(
    Option => #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")],
    Option<bool> => #[serde(rename = "bool")],
)]
#[derive(serde::Serialize)]
struct Data {
    a: Option<String>,
    b: Option<u64>,
    c: Option<String>,
    d: Option<bool>,
}
```

The `apply` attribute will expand into this, applying the attributs to the matching fields:

```rust
#[derive(serde::Serialize)]
struct Data {
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    a: Option<String>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    b: Option<u64>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    c: Option<String>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "bool")]
    d: Option<bool>,
}
```

bors r+

Co-authored-by: Jonas Bushart <jonas@bushart.org>
@jonasbb
Copy link
Owner Author

jonasbb commented May 3, 2023

This is available since a bit as serde_with::apply.

@jonasbb jonasbb closed this as completed May 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants