Skip to content

Commit

Permalink
Add from_word and from_none options
Browse files Browse the repository at this point in the history
This allows overriding how FromMeta-deriving types handle shorthand declarations or being absent altogether.

Previously, this was done using `with` on each field where the type was consumed, but that could cause attribute sprawl or inconsistent behavior when consuming the type.

This commit includes validations to avoid conflicts for unit structs, newtype structs, and enums that use the `word` variant-level attribute.

Fixes #320
  • Loading branch information
TedDriggs committed Jan 22, 2025
1 parent cde0cb6 commit d135cdc
Show file tree
Hide file tree
Showing 8 changed files with 353 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Support `#[darling(with = ...)]` on the `data` field when deriving `FromDeriveInput`. This allows the use of simpler receiver types, such as a `Vec` of enum variants.
- Bump version of `proc-macro2` to 1.0.86.
- Accept closures for `#[darling(with = ...)]` on fields in `FromDeriveInput`, `FromMeta`, `FromField`, etc. [#309](https://github.com/TedDriggs/darling/issues/309)
- Add `darling::util::Callable` to accept a path or closure as a meta-item expression
- Add `#[darling(from_word = ...)]` and `#[darling(from_none = ...)]` to control shorthand and fallback behaviors for structs and enums deriving `FromMeta` [#320](https://github.com/TedDriggs/darling/issues/320)

## v0.20.10 (July 9, 2024)

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ Darling's features are built to work well for real-world projects.
- **Span access**: Use `darling::util::SpannedValue` in a struct to get access to that meta item's source code span. This can be used to emit warnings that point at a specific field from your proc macro. In addition, you can use `darling::Error::write_errors` to automatically get precise error location details in most cases.
- **"Did you mean" suggestions**: Compile errors from derived darling trait impls include suggestions for misspelled fields.
- **Struct flattening**: Use `#[darling(flatten)]` to remove one level of structure when presenting your meta item to users. Fields that are not known to the parent struct will be forwarded to the `flatten` field.
- **Custom shorthand**: Use `#[darling(from_word = ...)]` on a struct or enum to override how a simple word is interpreted. By default, it is an error for your macro's user to fail to specify the fields of your struct, but with this you can choose to instead produce a set of default values. This takes either a path or a closure whose signature matches `FromMeta::from_word`.
- **Custom handling for missing fields**: When a field is not present and `#[darling(default)]` is not used, derived impls will call `FromMeta::from_none` on that field's type to try and get the fallback value for the field. Usually, there is not a fallback value, so a missing field error is generated. `Option<T: FromMeta>` uses this to make options optional without requiring `#[darling(default)]` declarations, and structs and enums can use this themselves with `#[darling(from_none = ...)]`. This takes either a path or a closure whose signature matches `FromMeta::from_none`.

## Shape Validation

Expand Down
46 changes: 35 additions & 11 deletions core/src/codegen/from_meta_impl.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;

use crate::ast::{Data, Fields, Style};
use crate::codegen::{Field, OuterFromImpl, TraitImpl, Variant};
use crate::util::Callable;

pub struct FromMetaImpl<'a> {
pub base: TraitImpl<'a>,
pub from_word: Option<&'a Callable>,
pub from_none: Option<&'a Callable>,
}

impl ToTokens for FromMetaImpl<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let base = &self.base;

let from_word = self.from_word.map(|body| {
quote_spanned! {body.span()=>
fn from_word() -> ::darling::Result<Self> {
::darling::export::identity::<fn() -> ::darling::Result<Self>>(#body)()
}
}
});

let from_none = self.from_none.map(|body| {
quote_spanned! {body.span()=>
fn from_none() -> ::darling::export::Option<Self> {
::darling::export::identity::<fn() -> ::darling::export::Option<Self>>(#body)()
}
}
});

let impl_block = match base.data {
// Unit structs allow empty bodies only.
Data::Struct(ref vd) if vd.style.is_unit() => {
Expand Down Expand Up @@ -55,6 +75,10 @@ impl ToTokens for FromMetaImpl<'_> {
let post_transform = base.post_transform_call();

quote!(
#from_word

#from_none

fn from_list(__items: &[::darling::export::NestedMeta]) -> ::darling::Result<Self> {

#decls
Expand Down Expand Up @@ -89,22 +113,22 @@ impl ToTokens for FromMetaImpl<'_> {
}
};

let word_or_err = variants
let from_word_method = variants
.iter()
.find_map(|variant| {
if variant.word {
let ty_ident = variant.ty_ident;
let variant_ident = variant.variant_ident;
Some(quote!(::darling::export::Ok(#ty_ident::#variant_ident)))
Some(quote! {
fn from_word() -> ::darling::Result<Self> {
::darling::export::Ok(#ty_ident::#variant_ident)
}
})
} else {
None
}
})
.unwrap_or_else(|| {
quote!(::darling::export::Err(
::darling::Error::unsupported_format("word")
))
});
.or(from_word);

let data_variants = variants.iter().map(Variant::as_data_match_arm);

Expand Down Expand Up @@ -135,9 +159,9 @@ impl ToTokens for FromMetaImpl<'_> {
}
}

fn from_word() -> ::darling::Result<Self> {
#word_or_err
}
#from_word_method

#from_none
)
}
};
Expand Down
78 changes: 64 additions & 14 deletions core/src/options/from_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ use crate::ast::Data;
use crate::codegen::FromMetaImpl;
use crate::error::Accumulator;
use crate::options::{Core, ParseAttribute, ParseData};
use crate::{Error, Result};
use crate::util::Callable;
use crate::{Error, FromMeta, Result};

pub struct FromMetaOptions {
base: Core,
/// Override for the default [`FromMeta::from_word`] method.
from_word: Option<Callable>,
/// Override for the default [`FromMeta::from_none`] method.
from_none: Option<Callable>,
}

impl FromMetaOptions {
pub fn new(di: &syn::DeriveInput) -> Result<Self> {
(FromMetaOptions {
base: Core::start(di)?,
from_word: None,
from_none: None,
})
.parse_attributes(&di.attrs)?
.parse_body(&di.data)
Expand All @@ -23,7 +30,25 @@ impl FromMetaOptions {

impl ParseAttribute for FromMetaOptions {
fn parse_nested(&mut self, mi: &syn::Meta) -> Result<()> {
self.base.parse_nested(mi)
let path = mi.path();

if path.is_ident("from_word") {
if self.from_word.is_some() {
return Err(Error::duplicate_field_path(path).with_span(path));
}

self.from_word = FromMeta::from_meta(mi).map(Some)?;
} else if path.is_ident("from_none") {
if self.from_none.is_some() {
return Err(Error::duplicate_field_path(path).with_span(path));
}

self.from_none = FromMeta::from_meta(mi).map(Some)?;
} else {
self.base.parse_nested(mi)?;
}

Ok(())
}
}

Expand All @@ -39,18 +64,41 @@ impl ParseData for FromMetaOptions {
fn validate_body(&self, errors: &mut Accumulator) {
self.base.validate_body(errors);

if let Data::Enum(ref data) = self.base.data {
// Adds errors for duplicate `#[darling(word)]` annotations across all variants.
let word_variants: Vec<_> = data
.iter()
.filter_map(|variant| variant.word.as_ref())
.collect();
if word_variants.len() > 1 {
for word in word_variants {
errors.push(
Error::custom("`#[darling(word)]` can only be applied to one variant")
.with_span(&word.span()),
);
match self.base.data {
Data::Struct(ref data) => {
if let Some(from_word) = &self.from_word {
if data.is_unit() {
errors.push(Error::custom("`from_word` cannot be used on unit structs because it conflicts with the generated impl").with_span(from_word));
} else if data.is_newtype() {
errors.push(Error::custom("`from_word` cannot be used on newtype structs because the implementation is entirely delegated to the inner type").with_span(from_word));
}
}
}
Data::Enum(ref data) => {
let word_variants: Vec<_> = data
.iter()
.filter_map(|variant| variant.word.as_ref())
.collect();

if !word_variants.is_empty() {
if let Some(from_word) = &self.from_word {
errors.push(
Error::custom(
"`from_word` cannot be used with an enum that also uses `word`",
)
.with_span(from_word),
)
}
}

// Adds errors for duplicate `#[darling(word)]` annotations across all variants.
if word_variants.len() > 1 {
for word in word_variants {
errors.push(
Error::custom("`#[darling(word)]` can only be applied to one variant")
.with_span(&word.span()),
);
}
}
}
}
Expand All @@ -61,6 +109,8 @@ impl<'a> From<&'a FromMetaOptions> for FromMetaImpl<'a> {
fn from(v: &'a FromMetaOptions) -> Self {
FromMetaImpl {
base: (&v.base).into(),
from_word: v.from_word.as_ref(),
from_none: v.from_none.as_ref(),
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
//! in values not specified by the caller.
//! * **Skipped fields**: You can skip a variant or field using `#[darling(skip)]`. Fields marked with this will fall back to
//! `Default::default()` for their value, but you can override that with an explicit default or a value from the type-level default.
//! * **Custom shorthand**: Use `#[darling(from_word = ...)]` on a struct or enum to override how a simple word is interpreted.
//! By default, it is an error for your macro's user to fail to specify the fields of your struct, but with this you can choose to
//! instead produce a set of default values. This takes either a path or a closure whose signature matches `FromMeta::from_word`.
//! * **Custom handling for missing fields**: When a field is not present and `#[darling(default)]` is not used, derived impls will
//! call `FromMeta::from_none` on that field's type to try and get the fallback value for the field. Usually, there is not a fallback
//! value, so a missing field error is generated. `Option<T: FromMeta>` uses this to make options optional without requiring
//! `#[darling(default)]` declarations, and structs and enums can use this themselves with `#[darling(from_none = ...)]`.
//! This takes either a path or a closure whose signature matches `FromMeta::from_none`.
//!
//! ## Forwarded Fields
//! All derivable traits except `FromMeta` support forwarding some fields from the input AST to the derived struct.
Expand Down
30 changes: 30 additions & 0 deletions tests/compile-fail/from_word.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use darling::FromMeta;

// This usage of `from_word` is invalid because unit structs already generate a from_word
// method, and we don't allow using the from_word override when it conflicts with the macro's
// "normal" operation.
#[derive(FromMeta)]
#[darling(from_word = || Ok(Unit))]
struct Unit;

fn newtype_from_word() -> darling::Result<Newtype> {
Ok(Newtype(true))
}

// This usage of `from_word` is invalid because newtype structs call the inner type's `from_meta`
// directly from their `from_meta`, so the custom `from_word` will never be called in normal usage.
#[derive(FromMeta)]
#[darling(from_word = newtype_from_word)]
struct Newtype(bool);

#[derive(FromMeta)]
#[darling(from_word = || Ok(Wordy::Options { thing: "Hello".to_string() }))]
enum Wordy {
#[darling(word)]
Normal,
Options {
thing: String,
},
}

fn main() {}
17 changes: 17 additions & 0 deletions tests/compile-fail/from_word.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
error: `from_word` cannot be used on unit structs because it conflicts with the generated impl
--> tests/compile-fail/from_word.rs:7:23
|
7 | #[darling(from_word = || Ok(Unit))]
| ^

error: `from_word` cannot be used on newtype structs because the implementation is entirely delegated to the inner type
--> tests/compile-fail/from_word.rs:17:23
|
17 | #[darling(from_word = newtype_from_word)]
| ^^^^^^^^^^^^^^^^^

error: `from_word` cannot be used with an enum that also uses `word`
--> tests/compile-fail/from_word.rs:21:23
|
21 | #[darling(from_word = || Ok(Wordy::Options { thing: "Hello".to_string() }))]
| ^
Loading

0 comments on commit d135cdc

Please sign in to comment.