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

Accept an arbitrary expression that returns a callable object in #[darling(with)] #309

Closed
Veetaha opened this issue Oct 1, 2024 · 4 comments · Fixed by #310
Closed

Accept an arbitrary expression that returns a callable object in #[darling(with)] #309

Veetaha opened this issue Oct 1, 2024 · 4 comments · Fixed by #310

Comments

@Veetaha
Copy link
Contributor

Veetaha commented Oct 1, 2024

It's not a big issue, but just an inconvenience. Here is an example of this problem that I have.

I need to configure parsing with some parameterization necessary for validation and error messages. Currently to do that you always need to define a separate function item that accepts a &syn::Meta and returns a Result. It would be better if I could just write an expression that returns a closure to invoke in the #[darling(with)] directly like this:

use darling::FromMeta;

#[derive(FromMeta)]
struct Example {
    #[darling(with = parse_item_params(... config here ...))]
    finish_fn: ItemParams,
   
    // alternatively just pass a closure 
    #[darling(with = |meta| parse_item_params_alt(meta, ... config here ...))]
    builder_type: ItemParams,
}

fn parse_item_params(config: ItemParamsParsing) -> impl FnOnce(&syn::Meta) -> Result<ItemParams> { /**/ }
fn parse_item_params_alt(meta: &syn::Meta, config: ItemParamsParsing) -> Result<ItemParams> { /**/ }

The main goal is to provide additional context for the parser function passed to with, and also keep that config as close to the field as possible lexically.

@TedDriggs
Copy link
Owner

TedDriggs commented Oct 2, 2024

One advantage of the closure approach is that closures and paths are syntactically disjoint - there is no way to confuse the two. That allows with = |meta| { ... } to work while maintaining backwards compatibility, which is pretty nice.

I would want to make sure the passed-in closure didn't have access to any locals, so I'm thinking the generated code would be something like...

EDIT: This doesn't work as-written; there are type annotations needed in the closure that are missing. Adding the annotation automatically seems possible but ugly.

struct Thing {
    #[darling(with = |meta| parse_with_alts(meta))]
    demo: Stuff
}

impl FromMeta for Thing {
    fn from_meta(meta: &Meta) -> Result<Self> {
        // PROBLEM: We need a return type for this function, but if someone uses
        // `with` in conjunction with `map` or `and_then`, we might not know the
        // interim return type.
        fn with_wrapper__demo(__meta: &Meta) -> Result<Stuff> {
            let closure = |meta| parse_with_alts(meta);
            // POSSIBLE SOLUTION: Add postfix call here, so that this function
            // will always have the return type of the field.
            closure(__meta)
        }

        // rest of the generated method goes here...
    }
}

A change here needs to take #305 into consideration, since that is also bothering me about with.

@Veetaha
Copy link
Contributor Author

Veetaha commented Oct 2, 2024

To make sure the closure doesn't capture anything it's possible to just require it to be assignable to a function pointer:

let closure: fn(_) -> _ = |meta| ...;

This way the closure also has access to the generic parameters if there are any on the struct/enum under the FromMeta. When you move it into a different function (even nested in the impl), the generic params from the outer item aren't available there.

Btw I don't see any ambiguity and problems with the compatibility this change may cause. Since today the syntax is limited to function paths, which are just a subset of an expression that returns a "callable object".

@TedDriggs
Copy link
Owner

require it to be assignable to a function pointer

TIL; thanks for sharing that.

A revised version of this is then...

struct Thing {
    #[darling(with = |meta| parse_with_alts(meta))]
    demo: Stuff
}

impl FromMeta for Thing {
    fn from_meta(meta: &Meta) -> Result<Self> {
        let with_closure__demo: fn(&Meta) -> _ = |meta| parse_with_alts(meta);

        // rest of the generated method goes here, 
        // using that `with_closure__demo` as if it was the passed path
    }
}

@TedDriggs
Copy link
Owner

I've posted a WIP PR for this #310

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