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

Add support for flatten #146

Closed
Veetaha opened this issue Aug 24, 2021 · 3 comments · Fixed by #270
Closed

Add support for flatten #146

Veetaha opened this issue Aug 24, 2021 · 3 comments · Fixed by #270

Comments

@Veetaha
Copy link
Contributor

Veetaha commented Aug 24, 2021

Suppose we have the following traits:

trait Foo { /* */ }
trait Bar: Foo { /* */ }

and we want to provide derive macros for both of them where #[derive(Foo)] derives only Foo trait and #[derive(Bar)] derives both Foo and Bar traits (basically #[derive(Bar)] automatically adds #[derive(Foo)] without the user needing to write additional derive explicitly.

When implementing #[derive(Bar)] we would also want to access the field or enum variant attributes that #[derive(Foo)] requires.

With darling this isn't very simple to do this for now.
As for now we would need to do it like this:

Long example
// Declaring derive input types for `Foo` is trivial

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(foo_bar))]
struct FooInput { /* regular usage of darling here */ }

#[derive(Debug, FromVariant)]
#[darling(attributes(foo_bar))]
struct FooVariant { /* regular usage of darling here */ }

#[derive(Debug, FromField)]
#[darling(attributes(foo_bar))]
struct FooField { /* regular usage of darling here */ }


// However derive input types for `Bar` need to be composed with `Foo` inputs
// and this isn't very trivial

#[derive(Debug)]
struct BarInput {
    foo: FooInput,
    bar: BarInputData
}

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(foo_bar))]
struct BarInputData {
   /* regular usage of darling here with attributes only specific to `Bar` derive */
}

impl FromDeriveInput for ItemInput {
    fn from_derive_input(input: &syn::DeriveInput) -> darling::Result<Self> {
        Ok(Self {
            foo: FromDeriveInput::from_derive_input(input)?,
            bar: FromDeriveInput::from_derive_input(input)?,
        })
    }
}

#[derive(Debug)]
struct BarField {
    foo: FooField,
    bar: BarFieldData,
}

#[derive(Debug, FromField)]
#[darling(attributes(foo_bar))]
struct BarFieldData {
   /* regular usage of darling here with attributes only specific to `Bar` derive */
}

impl FromField for BarField {
    fn from_field(field: &Field) -> darling::Result<Self> {
        Ok(Self {
            foo: FromField::from_field(field)?,
            bar: FromField::from_field(field)?,
        })
    }
}

// Same story for `BarVariant`, it's definition is ommitted here...

Instead of this I propose to add #[darling(flatten)] attribute that works similar to serde's analogue.

Something like this:

// Declaring derive input types for `Foo` is trivial

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(foo_bar))]
struct FooInput { /* regular usage of darling here */ }

#[derive(Debug, FromVariant)]
#[darling(attributes(foo_bar))]
struct FooVariant { /* regular usage of darling here */ }

#[derive(Debug, FromField)]
#[darling(attributes(foo_bar))]
struct FooField { /* regular usage of darling here */ }

// Declaring derive input types for `Bar` is also trivial

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(foo_bar))]
struct BarInput {
    /* regular usage of darling here */
    #[darling(flatten)]
    foo: FooInput,
}

#[derive(Debug, FromVariant)]
#[darling(attributes(foo_bar))]
struct BarVariant {
    /* regular usage of darling here */
    #[darling(flatten)]
    foo: FooVariant,
}

#[derive(Debug, FromField)]
#[darling(attributes(foo_bar))]
struct BarField {
    /* regular usage of darling here */
    #[darling(flatten)]
    foo: FooField,
}

However, there are still open questions about how #[darling(attributes, supports)] should behave when flattening and there will also be several ident, attrs and other data extracted from the parsed field, derive input or enum variant which make flattening feature no so straightforward...

@TedDriggs
Copy link
Owner

darling takes a lot of inspiration from serde, and flatten is something I've personally used in serde on many occasions. If there were a clean path to add it to darling, I'd be very supportive.

I don't think this is possible without a fundamental rework of darling's API. In darling, the composable core trait FromMeta's methods all return darling::Result<Self> - there's no state machine, like there is in serde. Making field suggestions would also require an API change; there's no support for a FromMeta exposing "expected fields", especially not without an instance of the type present.

I'm open to proposals on how to implement this, as long as:

  • It doesn't require breaking the FromMeta trait
  • It doesn't come with a lot of caveats or surprising behaviors
  • It doesn't come at the expense of good error messages

@TedDriggs
Copy link
Owner

TedDriggs commented Apr 19, 2022

I have an idea of how to do this in a limited capacity.

When from_list is iterating through Meta values, it currently calls Error::unknown_field_with_alts for any field it doesn't recognize (source).

If darling allowed one field to get #[darling(flatten)] on it, then fields which would otherwise have become errors could instead be collected into a Vec, and then passed to the flattened field's FromMeta::from_list method.

// #[sample(known, known_2 = "...", unknown_1, unknown_2 = "..."))]

#[derive(FromMeta)]
struct A {
    known: Flag,
    known_2: String,
    #[darling(flatten)]
    other: B,
}

#[derive(FromMeta)]
struct B {
    unknown_1: Flag,
    unknown_2: String,
}

Pitfalls

  1. "Did you mean" suggestions will come from the inner-most flatten, and won't include the outer struct's fields. That feels pretty bad.
  2. This would only work for structs with named fields and maps; it wouldn't work for enums, tuple structs, etc.

Open Questions

  1. If no fields are passed to the flattened member, what happens?
  2. Where is this valid? FromMeta for sure - how about FromDeriveInput,FromField, et alia?

@TedDriggs
Copy link
Owner

"Did you mean" suggestions will come from the inner-most flatten, and won't include the outer struct's fields. That feels pretty bad.

This could be possible to mitigate by adding a function to darling::Error which would allow for amending the error with additional field suggestions; the outer struct would attempt to add its "did you mean" items to any errors returned by inner, and the method would internally handle checking the error type and only adding the options when appropriate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants