Skip to content

Commit

Permalink
Improve FromMeta implementation for enums (#260)
Browse files Browse the repository at this point in the history
* Add #[darling(word)] to allow marking a variant as the one when a shorthand property is observed
* Improve validation and parsing for enums with heterogeneous variant shapes
  • Loading branch information
davidsemakula committed Jan 19, 2024
1 parent 0c79e55 commit 6118a83
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 4 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

- Accept bare paths in `#[darling(default = ...)]` [#258](https://github.com/TedDriggs/darling/pull/258)
- Add `FromMeta` impl for `PathBuf` [#259](https://github.com/TedDriggs/darling/pull/259)
- Improve `FromMeta` implementation for enums [#260](https://github.com/TedDriggs/darling/pull/260)
- Properly implement unit arms in `FromMeta::from_list` to provide a consistent API for heterogeneous enums that include a mix of unit, newtype and struct variants
- Add `#[darling(word)]` attribute for unit enum variants (See [#63](https://github.com/TedDriggs/darling/issues/63) for details)

## v0.20.3 (July 12, 2023)

Expand Down
26 changes: 23 additions & 3 deletions core/src/codegen/from_meta_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ impl<'a> ToTokens for FromMetaImpl<'a> {
}
Data::Enum(ref variants) => {
let unit_arms = variants.iter().map(Variant::as_unit_match_arm);
let struct_arms = variants.iter().map(Variant::as_data_match_arm);

let unknown_variant_err = if !variants.is_empty() {
let names = variants.iter().map(Variant::as_name);
Expand All @@ -90,16 +89,33 @@ impl<'a> ToTokens for FromMetaImpl<'a> {
}
};

let word_or_err = 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)))
} else {
None
}
})
.unwrap_or_else(|| {
quote!(::darling::export::Err(
::darling::Error::unsupported_format("word")
))
});

quote!(
fn from_list(__outer: &[::darling::export::NestedMeta]) -> ::darling::Result<Self> {
// An enum must have exactly one value inside the parentheses if it's not a unit
// match arm
// match arm.
match __outer.len() {
0 => ::darling::export::Err(::darling::Error::too_few_items(1)),
1 => {
if let ::darling::export::NestedMeta::Meta(ref __nested) = __outer[0] {
match ::darling::util::path_to_string(__nested.path()).as_ref() {
#(#struct_arms)*
#(#variants)*
__other => ::darling::export::Err(::darling::Error::#unknown_variant_err.with_span(__nested))
}
} else {
Expand All @@ -116,6 +132,10 @@ impl<'a> ToTokens for FromMetaImpl<'a> {
__other => ::darling::export::Err(::darling::Error::unknown_value(__other))
}
}

fn from_word() -> ::darling::Result<Self> {
#word_or_err
}
)
}
};
Expand Down
14 changes: 14 additions & 0 deletions core/src/codegen/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub struct Variant<'a> {
/// Whether or not the variant should be skipped in the generated code.
pub skip: bool,

/// Whether or not the variant should be used to create an instance for
/// `FromMeta::from_word`.
pub word: bool,

pub allow_unknown_fields: bool,
}

Expand Down Expand Up @@ -53,6 +57,16 @@ impl<'a> UsesTypeParams for Variant<'a> {
}
}

impl<'a> ToTokens for Variant<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.data.is_unit() {
self.as_unit_match_arm().to_tokens(tokens);
} else {
self.as_data_match_arm().to_tokens(tokens)
}
}
}

/// Code generator for an enum variant in a unit match position.
/// This is placed in generated `from_string` calls for the parent enum.
/// Value-carrying variants wrapped in this type will emit code to produce an "unsupported format" error.
Expand Down
22 changes: 21 additions & 1 deletion core/src/options/from_meta.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use proc_macro2::TokenStream;
use quote::ToTokens;

use crate::ast::Data;
use crate::codegen::FromMetaImpl;
use crate::error::Accumulator;
use crate::options::{Core, ParseAttribute, ParseData};
use crate::Result;
use crate::{Error, Result};

pub struct FromMetaOptions {
base: Core,
Expand Down Expand Up @@ -33,6 +35,24 @@ impl ParseData for FromMetaOptions {
fn parse_field(&mut self, field: &syn::Field) -> Result<()> {
self.base.parse_field(field)
}

fn validate_body(&self, errors: &mut Accumulator) {
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()),
);
}
}
}
}
}

impl<'a> From<&'a FromMetaOptions> for FromMetaImpl<'a> {
Expand Down
22 changes: 22 additions & 0 deletions core/src/options/input_variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::borrow::Cow;
use crate::ast::Fields;
use crate::codegen;
use crate::options::{Core, InputField, ParseAttribute};
use crate::util::SpannedValue;
use crate::{Error, FromMeta, Result};

#[derive(Debug, Clone)]
Expand All @@ -11,6 +12,9 @@ pub struct InputVariant {
attr_name: Option<String>,
data: Fields<InputField>,
skip: Option<bool>,
/// Whether or not the variant should be used to create an instance for
/// `FromMeta::from_word`.
pub word: Option<SpannedValue<bool>>,
/// Whether or not unknown fields are acceptable in this
allow_unknown_fields: Option<bool>,
}
Expand All @@ -26,6 +30,7 @@ impl InputVariant {
.map_or_else(|| Cow::Owned(self.ident.to_string()), Cow::Borrowed),
data: self.data.as_ref().map(InputField::as_codegen_field),
skip: self.skip.unwrap_or_default(),
word: *self.word.unwrap_or_default(),
allow_unknown_fields: self.allow_unknown_fields.unwrap_or_default(),
}
}
Expand All @@ -36,6 +41,7 @@ impl InputVariant {
attr_name: Default::default(),
data: Fields::empty_from(&v.fields),
skip: Default::default(),
word: Default::default(),
allow_unknown_fields: None,
})
.parse_attributes(&v.attrs)?;
Expand Down Expand Up @@ -95,6 +101,22 @@ impl ParseAttribute for InputVariant {
}

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

if !self.data.is_unit() {
let note = "`#[darling(word)]` can only be applied to a unit variant";
#[cfg(feature = "diagnostics")]
let error = Error::unknown_field_path(path).note(note);
#[cfg(not(feature = "diagnostics"))]
let error = Error::custom(format!("Unexpected field: `word`. {}", note));

return Err(error.with_span(mi));
}

self.word = FromMeta::from_meta(mi)?;
} else {
return Err(Error::unknown_field_path(path).with_span(mi));
}
Expand Down
9 changes: 9 additions & 0 deletions core/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use proc_macro2::Span;
use syn::{parse_quote, spanned::Spanned};

use crate::ast::NestedMeta;
use crate::error::Accumulator;
use crate::{Error, FromMeta, Result};

mod core;
Expand Down Expand Up @@ -132,6 +133,8 @@ pub trait ParseData: Sized {
Data::Union(_) => unreachable!(),
};

self.validate_body(&mut errors);

errors.finish_with(self)
}

Expand All @@ -146,4 +149,10 @@ pub trait ParseData: Sized {
fn parse_field(&mut self, field: &syn::Field) -> Result<()> {
Err(Error::unsupported_format("struct field").with_span(field))
}

/// Perform validation checks that require data from more than one field or variant.
/// The default implementation does no validations.
/// Implementors can override this method as appropriate for their use-case.
#[allow(unused_variables)]
fn validate_body(&self, errors: &mut Accumulator) {}
}
79 changes: 79 additions & 0 deletions examples/heterogeneous_enum_and_word.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! This example demonstrates:
//!
//! - The behavior of a derived `FromMeta` implementation for heterogeneous enums
//! (i.e. enums that include a mix of unit, newtype and struct variants).
//! - Using `#[darling(word)]` to specify a unit variant to use when a receiver field
//! is specified without a value (i.e. a unit variant to use for deriving the
//! `FromMeta::from_word` method).
//! - Using `#[darling(default)]` on a receiver field to fall back to `Default::default()`
//! for the enum's value when the receiver field is not specified by the caller.

use darling::{Error, FromDeriveInput, FromMeta};
use syn::parse_quote;

/// A playback volume.
#[derive(Debug, FromMeta, PartialEq, Eq)]
enum Volume {
Normal,
#[darling(word)]
Low,
High,
#[darling(rename = "dB")]
Decibels(u8),
}

impl Default for Volume {
fn default() -> Self {
Volume::Normal
}
}

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(play))]
struct PlayReceiver {
#[darling(default)]
volume: Volume,
}

fn main() {
// `Default::default()` is used when `volume` is not specified.
let missing_volume = PlayReceiver::from_derive_input(&parse_quote! {
#[play]
struct Player;
})
.unwrap();
assert_eq!(Volume::Normal, missing_volume.volume);

// `#[darling(word)]` unit variant is used when `volume` is specified as a word with no value.
let empty_volume = PlayReceiver::from_derive_input(&parse_quote! {
#[play(volume)]
struct Player;
})
.unwrap();
assert_eq!(Volume::Low, empty_volume.volume);

// Specified `volume` value is used when provided.
let unit_variant_volume = PlayReceiver::from_derive_input(&parse_quote! {
#[play(volume(high))]
struct Player;
})
.unwrap();
assert_eq!(Volume::High, unit_variant_volume.volume);
let newtype_volume = PlayReceiver::from_derive_input(&parse_quote! {
#[play(volume(dB = 100))]
struct Player;
})
.unwrap();
assert_eq!(Volume::Decibels(100), newtype_volume.volume);

// Multiple `volume` values result in an error.
let err = PlayReceiver::from_derive_input(&parse_quote! {
#[play(volume(low, dB = 20))]
struct Player;
})
.unwrap_err();
assert_eq!(
err.to_string(),
Error::too_many_items(1).at("volume").to_string()
);
}
12 changes: 12 additions & 0 deletions tests/compile-fail/duplicate_word_across_variants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use darling::FromMeta;

#[derive(FromMeta)]
enum Choice {
#[darling(word)]
A,
#[darling(word)]
B,
C,
}

fn main() {}
11 changes: 11 additions & 0 deletions tests/compile-fail/duplicate_word_across_variants.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: `#[darling(word)]` can only be applied to one variant
--> tests/compile-fail/duplicate_word_across_variants.rs:5:15
|
5 | #[darling(word)]
| ^^^^

error: `#[darling(word)]` can only be applied to one variant
--> tests/compile-fail/duplicate_word_across_variants.rs:7:15
|
7 | #[darling(word)]
| ^^^^
10 changes: 10 additions & 0 deletions tests/compile-fail/duplicate_word_on_variant.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use darling::FromMeta;

#[derive(FromMeta)]
enum Choice {
#[darling(word, word)]
A,
B,
}

fn main() {}
5 changes: 5 additions & 0 deletions tests/compile-fail/duplicate_word_on_variant.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: Duplicate field `word`
--> tests/compile-fail/duplicate_word_on_variant.rs:5:21
|
5 | #[darling(word, word)]
| ^^^^
10 changes: 10 additions & 0 deletions tests/compile-fail/word_on_wrong_variant_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use darling::FromMeta;

#[derive(FromMeta)]
enum Meta {
Unit,
#[darling(word)]
NotUnit(String)
}

fn main() {}
5 changes: 5 additions & 0 deletions tests/compile-fail/word_on_wrong_variant_type.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: Unexpected field: `word`. `#[darling(word)]` can only be applied to a unit variant
--> tests/compile-fail/word_on_wrong_variant_type.rs:6:15
|
6 | #[darling(word)]
| ^^^^
44 changes: 44 additions & 0 deletions tests/enums_default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use darling::{FromDeriveInput, FromMeta};
use syn::parse_quote;

#[derive(Debug, FromMeta, PartialEq, Eq)]
enum Dolor {
Sit,
#[darling(word)]
Amet,
}

impl Default for Dolor {
fn default() -> Self {
Dolor::Sit
}
}

#[derive(FromDeriveInput)]
#[darling(attributes(hello))]
struct Receiver {
#[darling(default)]
example: Dolor,
}

#[test]
fn missing_meta() {
let di = Receiver::from_derive_input(&parse_quote! {
#[hello]
struct Example;
})
.unwrap();

assert_eq!(Dolor::Sit, di.example);
}

#[test]
fn empty_meta() {
let di = Receiver::from_derive_input(&parse_quote! {
#[hello(example)]
struct Example;
})
.unwrap();

assert_eq!(Dolor::Amet, di.example);
}

0 comments on commit 6118a83

Please sign in to comment.