Skip to content

Commit

Permalink
Accept a closure for with in lieu of a path for fields
Browse files Browse the repository at this point in the history
Fixes #309
  • Loading branch information
TedDriggs committed Oct 2, 2024
1 parent c330c34 commit 61a4447
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 5 deletions.
7 changes: 7 additions & 0 deletions core/src/codegen/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub struct Field<'a> {
/// The type of the field in the input.
pub ty: &'a Type,
pub default_expression: Option<DefaultExpression<'a>>,
/// Initial declaration for `with`; this is used if `with` was a closure,
/// to assign the closure to a local variable.
pub with_initializer: Option<syn::Stmt>,
pub with_path: Cow<'a, Path>,
pub post_transform: Option<&'a PostfixTransform>,
pub skip: bool,
Expand Down Expand Up @@ -107,6 +110,10 @@ impl<'a> ToTokens for Declaration<'a> {
let mut __flatten: Vec<::darling::ast::NestedMeta> = vec![];
});
}

if let Some(stmt) = &self.0.with_initializer {
stmt.to_tokens(tokens);
}
}
}

Expand Down
61 changes: 58 additions & 3 deletions core/src/options/input_field.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::borrow::Cow;

use quote::format_ident;
use syn::{parse_quote_spanned, spanned::Spanned};

use crate::codegen;
Expand All @@ -13,7 +14,7 @@ pub struct InputField {
pub attr_name: Option<String>,
pub ty: syn::Type,
pub default: Option<DefaultExpression>,
pub with: Option<syn::Path>,
pub with: Option<With>,

/// If `true`, generated code will not look for this field in the input meta item,
/// instead always falling back to either `InputField::default` or `Default::default`.
Expand All @@ -34,7 +35,8 @@ impl InputField {
.map_or_else(|| Cow::Owned(self.ident.to_string()), Cow::Borrowed),
ty: &self.ty,
default_expression: self.as_codegen_default(),
with_path: self.with.as_ref().map_or_else(
with_initializer: self.with.as_ref().and_then(With::to_closure_declaration),
with_path: self.with.as_ref().map(|w| &w.path).map_or_else(
|| {
Cow::Owned(
parse_quote_spanned!(self.ty.span()=> ::darling::FromMeta::from_meta),
Expand Down Expand Up @@ -149,7 +151,7 @@ impl ParseAttribute for InputField {
return Err(Error::duplicate_field_path(path).with_span(mi));
}

self.with = Some(FromMeta::from_meta(mi)?);
self.with = Some(With::from_meta(&self.ident, mi)?);

if self.flatten.is_present() {
return Err(
Expand Down Expand Up @@ -239,3 +241,56 @@ impl ParseAttribute for InputField {
Ok(())
}
}

#[derive(Debug, Clone)]
pub struct With {
/// The path that generated code should use when calling this.
path: syn::Path,
/// If set, the closure that should be assigned to `path` locally.
closure: Option<syn::ExprClosure>,
}

impl With {
pub fn from_meta(field_name: &syn::Ident, meta: &syn::Meta) -> Result<Self> {
if let syn::Meta::NameValue(nv) = meta {
match &nv.value {
syn::Expr::Path(path) => Ok(Self::from(path.path.clone())),
syn::Expr::Closure(closure) => Ok(Self {
path: format_ident!("__with_closure_for_{}", field_name).into(),
closure: Some(closure.clone()),
}),
_ => Err(Error::unexpected_expr_type(&nv.value)),
}
} else {
Err(Error::unsupported_format("non-value"))
}
}

/// Create the statement that declares the closure as a function pointer.
fn to_closure_declaration(&self) -> Option<syn::Stmt> {
self.closure.as_ref().map(|c| {
let path = &self.path;
// An explicit annotation that the input is borrowed is needed here,
// or attempting to pass a closure will fail with an issue about a temporary
// value being dropped while still borrowed in the extractor loop.
//
// Making the parameter type explicit here avoids errors if the closure doesn't
// do enough to make the type clear to the compiler.
//
// The explicit return type is needed, or else using `Ok` and `?` in the closure
// body will produce an error about needing type annotations due to uncertainty
// about the error variant's type. `T` is left undefined so that postfix transforms
// still work as expected
parse_quote_spanned!(c.span()=> let #path: fn(&::syn::Meta) -> ::darling::Result<_> = #c;)
})
}
}

impl From<syn::Path> for With {
fn from(path: syn::Path) -> Self {
Self {
path,
closure: None,
}
}
}
11 changes: 9 additions & 2 deletions examples/expr_with.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
use darling::{util::parse_expr, FromDeriveInput};
use darling::{util::parse_expr, FromDeriveInput, FromMeta};
use syn::{parse_quote, Expr};

#[derive(FromDeriveInput)]
#[darling(attributes(demo))]
pub struct Receiver {
#[darling(with = parse_expr::preserve_str_literal, map = Some)]
example1: Option<Expr>,
#[darling(
// A closure can be used in lieu of a path.
with = |m| Ok(String::from_meta(m)?.to_uppercase()),
default
)]
example2: String,
}

fn main() {
let input = Receiver::from_derive_input(&parse_quote! {
#[demo(example1 = test::path)]
#[demo(example1 = test::path, example2 = "hello")]
struct Example;
})
.unwrap();

assert_eq!(input.example1, Some(parse_quote!(test::path)));
assert_eq!(input.example2, "HELLO".to_string());
}
33 changes: 33 additions & 0 deletions tests/meta_with.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use darling::{util::parse_expr, FromDeriveInput, FromMeta};
use syn::{parse_quote, Expr};

#[derive(FromDeriveInput)]
#[darling(attributes(demo))]
pub struct Receiver {
#[darling(with = parse_expr::preserve_str_literal, map = Some)]
example1: Option<Expr>,
#[darling(
with = |m| Ok(String::from_meta(m)?.to_uppercase()),
map = Some
)]
example2: Option<String>,
// This is deliberately strange - it keeps the field name, and ignores
// the rest of the attribute. In normal operation, this is strongly discouraged.
// It's used here to verify that the parameter type is known even if it can't be
// inferred from usage within the closure.
#[darling(with = |m| Ok(m.path().clone()))]
example3: syn::Path,
}

#[test]
fn handles_all_cases() {
let input = Receiver::from_derive_input(&parse_quote! {
#[demo(example1 = test::path, example2 = "hello", example3)]
struct Example;
})
.unwrap();

assert_eq!(input.example1, Some(parse_quote!(test::path)));
assert_eq!(input.example2, Some("HELLO".to_string()));
assert_eq!(input.example3, parse_quote!(example3));
}

0 comments on commit 61a4447

Please sign in to comment.