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

Improve FromMeta implementation for enums #260

Merged
merged 14 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
davidsemakula marked this conversation as resolved.
Show resolved Hide resolved
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>>,
davidsemakula marked this conversation as resolved.
Show resolved Hide resolved
/// 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";
TedDriggs marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "diagnostics")]
davidsemakula marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Loading