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

FromAttributes derive macro is underdocumented. #156

Closed
vi opened this issue Feb 22, 2022 · 15 comments
Closed

FromAttributes derive macro is underdocumented. #156

vi opened this issue Feb 22, 2022 · 15 comments

Comments

@vi
Copy link

vi commented Feb 22, 2022

Used naively, it just bails with error: FromAttributes without attributes collects nothing and there seems to be no solid example of both the code and input sample, neither in README nor on docs.rs.

@vi
Copy link
Author

vi commented Feb 22, 2022

For example, having input like

#[my_attribute]
struct Foo{}

/// My doccomment
struct Bar{}

#[my_attribute]
/// Both attribute and a doccomment
struct Baz;

how do I parse it into

#[derive(FromAttributes)]
struct MyAttrs {
    my_attribute: Option<()>,
    doc: Option<String>,
}

I tried to juggle attributes, forward_attrs (what's the difference?) around, but don't understand the inner logic.

@TedDriggs
Copy link
Owner

Thanks for calling this out - I added FromAttributes to unblock an individual user and haven't had a chance to update the docs.

Primarily, darling is built for crates that want to do serde-style attribute parsing, e.g.

#[my_crate(opt1, opt2 = "foo")]
pub struct Bar;

That would parse into a struct like this:

#[derive(FromAttributes)]
#[darling(attributes(my_crate))]
struct Options {
    opt1: Flag,
    opt2: String,
}

The advantage of using darling here is that the following representations are equivalent:

#[my_crate(opt1)]
#[my_crate(opt2 = "foo")]
pub struct Bar;

#[my_crate(opt2 = "foo")]
#[my_crate(opt1)]
pub struct Bar;

#[my_crate(opt2 = "foo", opt1)]
pub struct Bar;

// et cetera

For your specific use-case, there's a problem: darling doesn't currently track the presence of an empty attribute; it'll look at #[my_attribute] in your example, but will see no inner Meta items to parse, and will then continue.

If this example is accurate, then I think you're looking for forward_attrs in FromDeriveInput, which says, "preserve these attributes into a field called attrs on my receiver struct."

#[derive(FromDeriveInput)]
#[darling(forward_attrs(my_attribute, doc)]
struct Options {
    attrs: Vec<Attribute>
}

Would that work for you?

@TedDriggs
Copy link
Owner

How's this for an example to put in the docs?

Example

This is an illustrative example of using FromAttributes for a real use-case, adding tracing and authorization to methods

#[derive(Default, FromAttributes)]
#[darling(attributes(aspect), default)]
pub struct MethodOpts {
    trace: bool,
    role: Option<string>,
}
pub struct AspectOpts {
    methods: util::WithOriginal<MethodOpts, ImplItemMethod>
}
impl TryFrom<ItemImpl> for AspectOps {
    type Error = darling::Error;
    
    fn try_from(item_impl: ItemImpl) -> darling::Result<Self> {
        let mut methods = vec![];
        let mut errors = vec![];
        for item in item_impl.items {
            if let ImplItem::Method(method) = item {
                match MethodOpts::from_attributes(&method.attrs) {
                    Ok(opts) => methods.push(WithOriginal::new(opts, method)),
                    Err(e) => errors.push(e),
                }
            }
        }
        if !errors.is_empty() {
            return Err(darling::Error::multiple(errors));
        }
        Ok(Self { methods })
    }
}
#[proc_macro_attribute]
pub fn aspect(input: TokenStream) -> TokenStream {
    let impl_item: ItemImpl = syn::parse_macro_input!(input);
    let options = match AspectOps::try_from(impl_item) {
        Ok(ops) => ops,
        Err(e) => {
            return e.write_errors().into();
        }
    };
    // at this point, darling's role is done; use `options.methods` to step
    // through each of the methods, adding the necessary code for the aspects
    // that were selected.
}

In another crate, this can now be consumed as follows:

#[aspect]
impl MyController {
    #[aspect(trace)]
    pub fn create(user: UserId, new_resource: Resource) -> Result<ResourceId, MyCreateError> {
        // elided
    }
    pub fn get(user: UserId, id: ResourceId) -> Result<ResourceId, MyGetError> {
        // elided
    }
    #[aspect(trace, role = "owner")]
    pub fn delete(user: UserId, id: ResourceId) -> Result<(), MyDeleteError> {
        // elided
    }
}

@vi
Copy link
Author

vi commented Feb 22, 2022

Maybe empty attribute sets like

#[derive(FromAttributes)]
#[darling(attributes(my_crate))]
struct Options {
}

should also be supported?

It should return Ok(Options) if at least one instance of #[my_crate] is found and Err if it is absent entirely.


Also capturing the doccomment using Darling is counterintuitive. Shall I use attributes(doc)?


Primarily, darling is built for crates that want to do serde-style attribute parsing

Maybe the scope should be extended to "declaratively parse everything that can be in #[] and is obtained from syn or proc_macro2"?

@vi
Copy link
Author

vi commented Feb 22, 2022

How's this for an example to put in the docs?

It looks like a second, more advanced example.

I think it should be prepended by something trivial-ish.

@vi
Copy link
Author

vi commented Feb 22, 2022

    #[my_crate(opt1)]
    #[my_crate(opt2 = "foo")]
    pub struct Bar;
    
    #[my_crate(opt2 = "foo")]
    #[my_crate(opt1)]
    pub struct Bar;
    
    #[my_crate(opt2 = "foo", opt1)]
    pub struct Bar;

Isn't there too many my_crate around? As it is not enforced by rules (proc macro can declare multiple attributes), a proc macro may be designed more like this:

#[derive(MyCrate)]
#[opt1]
#[opt2="foo"]
pub struct Bar;

#[derive(MyCrate)]
#[opt1]
#[opt2="foo"]
pub struct Bar;

#[derive(MyCrate)]
#[opt2="foo"]
#[opt1]
pub struct Bar;

without annoying repetition of my_crate around. Maybe it is a poor code style for derive macros (such mini-attribute proliferation may lead to clashes between derive macros), but I expect Darling to be able to pack it into something like

#[derive(FromAttributes)]
#[darling(root)]
struct Options {
    opt1: Flag,
    opt2: String,
}

.

It may also be useful for capturing foreign attributes (defined not by your crate, but "spying" on somebody else's attributes). Being able to flatten them into one structure may be nice:

#[my_crate(opt2 = "foo")]
#[my_crate(opt1)]
#[other_crate(opt3="bar")]
#[freestanding_attribute]
pub struct Bar;
#[derive(FromAttributes)]
#[darling(attributes(my_crate))]
struct Options {
    opt1: Flag,
    opt2: String,
    #[darling(override_attribute="other_crate")]
    opt3: Option<String>,
    #[darling(root, rename="freestanding_attribute")]
    f_a: Flag,
}

@TedDriggs
Copy link
Owner

I think it should be prepended by something trivial-ish.

Do you have something in mind? Most simpler cases should be done with FromDeriveInput, so only advanced scenarios end up with FromAttributes, and those typically require machinery not provided by the crate in order to function end-to-end.


Let's set the empty-attribute case aside (or file a new issue specifically for it).


Also capturing the doccomment using Darling is counterintuitive. Shall I use attributes(doc)?

If deriving FromDeriveInput, you'd do that using #[darling(forward_attrs(doc))] and then reading the attrs field to find all those where attr.path.is_ident("doc"). If deriving FromAttributes, you'd handle that outside, potentially in the TryFrom block using the same attr.path.is_ident("doc") guard.


Primarily, darling is built for crates that want to do serde-style attribute parsing

Maybe the scope should be extended to "declaratively parse everything that can be in #[] and is obtained from syn or proc_macro2"?

Isn't there too many my_crate around?

This is a very reasonable question. When darling was first created, serde-style attributes for derive macros were the standard in Rust, but diesel and others have shifted the trend towards terser attributes with less nesting.

I don't plan to broaden the scope.

  • I don't think there are too many my_crate entries around, as long as one uses the combined form of #[my_crate(opt1, opt2 = "foo")].
  • As you've already observed, namespaceless attributes create a major risk of collision: serde, builder and more all have meta-items called default, and it would be impossible to disambiguate them without the initial attribute.
  • There is a solution in syn for parsing flatter attributes; a lot of the complexity darling mitigates comes from dealing with more complex structures
  • A number of darling's features around errors will rely on knowing that all meta items within the attribute are intended for darling to understand. For example, it's impossible to know whether defaultt is a typo by the user, or an attribute for a different macro to consume.

@vi
Copy link
Author

vi commented Feb 22, 2022

Most simpler cases should be done with FromDeriveInput, so only advanced scenarios end up with FromAttributes...

I just saw the type I got at hand was Vec<syn::Attribute> (from getting syn::File and descending to individual struct). So, having Vec<syn::Attribute>, I assumed I need FromAttributes - the signature matches at least. I don't see syn::Meta or something like that in Debug output at least.

@vi
Copy link
Author

vi commented Feb 22, 2022

There is a solution in syn for parsing flatter attributes; a lot of the complexity darling mitigates comes from dealing with more complex structures

Where is it? Does syn itself contain derive macro or special helpers allowing to parse those things not token by token? If yes, it should be liked from Darling's docs, explaining how that that syn feature complements Darling's use case.

@vi
Copy link
Author

vi commented Feb 22, 2022

I don't plan to broaden the scope.

README's opening phrase:

darling is a crate for proc macro authors, which enables parsing attributes into structs.

From this one can assume that any attributes can be Darling-ized, just like almost any JSON can be Serde-ized. This scope limitation should be documented early and explicitly.

@vi
Copy link
Author

vi commented Feb 22, 2022

I think it should be prepended by something trivial-ish.

Do you have something in mind?

Something like the opening example in README, but using FromAttributes instead of FromDeriveInput.

@TedDriggs
Copy link
Owner

Where is it? Does syn itself contain derive macro or special helpers allowing to parse those things not token by token? If yes, it should be liked from Darling's docs, explaining how that that syn feature complements Darling's use case.

I'd be happy to accept a PR including this in the documentation; I haven't used the syn functionality personally, so I'm not in a position to write a well-informed comparison of the options.


I just saw the type I got at hand was Vec<syn::Attribute>

Was there a parent DeriveInput in the debug output? I'll look at making the guidance in the docs for FromAttributes more explicit about preferring FromDeriveInput in any case where structs or enums are in use.


From this one can assume that any attributes can be Darling-ized, just like almost any JSON can be Serde-ized. This scope limitation should be documented early and explicitly.

I don't know that I agree this rises to the level of a scope limitation, as others have found value with the crate as-is. Even if you choose not to use its derivations, you can use its traits to model how to parse information from the AST.

@vi
Copy link
Author

vi commented Feb 22, 2022

I just saw the type I got at hand was Vecsyn::Attribute

Was there a parent DeriveInput in the debug output? I'll look at making the guidance in the docs for FromAttributes more explicit about preferring FromDeriveInput in any case where structs or enums are in use.

No, as it is not a proc macro at all. It is just proc_macro2 crate parsing output of rustc -Zunpretty=expanded, followed by turning it into a syn::File.


others have found value with the crate as-is

Obviously, the crate provide value as is for many use cases. It is already used in other part of the project (in a proc_macro crate, in FromField and FromDeriveInput modes). It just feels that only some minor part is missing to cover the full range of possible uses.

It is understandable if Darling does not support parsing something like #[my_crate(Test<>(&mut macro){})], just like JSON does not support parsing things like {[]:null,, 4+5}. But current limitation (supporting only two-level hierarchies, not single-level) is more like JSON parser supporting only parsing JSON objects, but not primitives or arrays. Sure, one can designate the library as "JSON object parser. Other types are supported inside object values", but that would decrease usefulness.

@TedDriggs
Copy link
Owner

JSON parser supporting only parsing JSON objects, but not primitives or arrays.

This is a good analogy; it's possible for serde to support encoding a struct as an array, but that requires a custom serializer and deserializer. Similarly, darling supports significant customization through composition of FromMeta (note that syn does provide the AttributeArgs struct, which gives a list of NestedMeta), but is more opinionated on what it supports via derivation.

@TedDriggs
Copy link
Owner

There's a bit more documentation for this now, and the larger conversation wandered in a direction that doesn't seem related to the original, so I'm going to close this out.

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

No branches or pull requests

2 participants