Skip to content

Commit

Permalink
Add #[darling(flatten)]
Browse files Browse the repository at this point in the history
Fixes #146
  • Loading branch information
TedDriggs committed Feb 22, 2024
1 parent 60bff90 commit 3eb3e93
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add `#[darling(flatten)]` to allow forwarding unknown fields to another struct [#146](https://github.com/TedDriggs/darling/issues/146)
- Don't suggest names of skipped fields in derived impls [#268](https://github.com/TedDriggs/darling/issues/268)

## v0.20.6 (February 14, 2024)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Darling's features are built to work well for real-world projects.
- **Multiple-occurrence fields**: Use `#[darling(multiple)]` on a `Vec` field to allow that field to appear multiple times in the meta-item. Each occurrence will be pushed into the `Vec`.
- **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.

## Shape Validation

Expand Down
91 changes: 60 additions & 31 deletions core/src/codegen/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ pub struct Field<'a> {
pub post_transform: Option<&'a PostfixTransform>,
pub skip: bool,
pub multiple: bool,
/// If set, this field will be given all unclaimed meta items and will
/// not be exposed as a standard named field.
pub flatten: bool,
}

impl<'a> Field<'a> {
/// Get the name of the meta item that should be matched against input and should be used in diagnostics.
///
/// This will be `None` if the field is `skip` or `flatten`, as neither kind of field is addressable
/// by name from the input meta.
pub fn as_name(&'a self) -> Option<&'a str> {
if self.skip {
if self.skip || self.flatten {
None
} else {
Some(&self.name_in_attr)
Expand All @@ -41,6 +48,10 @@ impl<'a> Field<'a> {
Declaration(self)
}

pub fn as_flatten_initializer(&'a self) -> FlattenInitializer<'a> {
FlattenInitializer(self)
}

pub fn as_match(&'a self) -> MatchArm<'a> {
MatchArm(self)
}
Expand Down Expand Up @@ -82,41 +93,60 @@ impl<'a> ToTokens for Declaration<'a> {
}
}

pub struct FlattenInitializer<'a>(&'a Field<'a>);

impl<'a> ToTokens for FlattenInitializer<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let field = self.0;
let ident = field.ident;

tokens.append_all(quote! {
#ident = (true, __errors.handle(::darling::FromMeta::from_list(__flatten.into_iter().cloned().map(::darling::ast::NestedMeta::Meta).collect::<Vec<_>>().as_slice())));
});
}
}

/// Represents an individual field in the match.
pub struct MatchArm<'a>(&'a Field<'a>);

impl<'a> ToTokens for MatchArm<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let field: &Field = self.0;
if !field.skip {
let name_str = &field.name_in_attr;
let ident = field.ident;
let with_path = &field.with_path;
let post_transform = field.post_transform.as_ref();

// Errors include the location of the bad input, so we compute that here.
// Fields that take multiple values add the index of the error for convenience,
// while single-value fields only expose the name in the input attribute.
let location = if field.multiple {
// we use the local variable `len` here because location is accessed via
// a closure, and the borrow checker gets very unhappy if we try to immutably
// borrow `#ident` in that closure when it was declared `mut` outside.
quote!(&format!("{}[{}]", #name_str, __len))
} else {
quote!(#name_str)
};

// Give darling's generated code the span of the `with_path` so that if the target
// type doesn't impl FromMeta, darling's immediate user gets a properly-spanned error.
//
// Within the generated code, add the span immediately on extraction failure, so that it's
// as specific as possible.
// The behavior of `with_span` makes this safe to do; if the child applied an
// even-more-specific span, our attempt here will not overwrite that and will only cost
// us one `if` check.
let extractor = quote_spanned!(with_path.span()=>#with_path(__inner)#post_transform.map_err(|e| e.with_span(&__inner).at(#location)));

tokens.append_all(if field.multiple {

// Skipped and flattened fields cannot be populated by a meta
// with their name, so they do not have a match arm.
if field.skip || field.flatten {
return;
}

let name_str = &field.name_in_attr;
let ident = field.ident;
let with_path = &field.with_path;
let post_transform = field.post_transform.as_ref();

// Errors include the location of the bad input, so we compute that here.
// Fields that take multiple values add the index of the error for convenience,
// while single-value fields only expose the name in the input attribute.
let location = if field.multiple {
// we use the local variable `len` here because location is accessed via
// a closure, and the borrow checker gets very unhappy if we try to immutably
// borrow `#ident` in that closure when it was declared `mut` outside.
quote!(&format!("{}[{}]", #name_str, __len))
} else {
quote!(#name_str)
};

// Give darling's generated code the span of the `with_path` so that if the target
// type doesn't impl FromMeta, darling's immediate user gets a properly-spanned error.
//
// Within the generated code, add the span immediately on extraction failure, so that it's
// as specific as possible.
// The behavior of `with_span` makes this safe to do; if the child applied an
// even-more-specific span, our attempt here will not overwrite that and will only cost
// us one `if` check.
let extractor = quote_spanned!(with_path.span()=>#with_path(__inner)#post_transform.map_err(|e| e.with_span(&__inner).at(#location)));

tokens.append_all(if field.multiple {
quote!(
#name_str => {
// Store the index of the name we're assessing in case we need
Expand All @@ -138,7 +168,6 @@ impl<'a> ToTokens for MatchArm<'a> {
}
)
});
}
}
}

Expand Down
30 changes: 27 additions & 3 deletions core/src/codegen/variant_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ use crate::codegen::Field;
pub struct FieldsGen<'a> {
fields: &'a Fields<Field<'a>>,
allow_unknown_fields: bool,
flatten_field: Option<&'a Field<'a>>,
}

impl<'a> FieldsGen<'a> {
pub fn new(fields: &'a Fields<Field<'a>>, allow_unknown_fields: bool) -> Self {
Self {
fields,
flatten_field: fields.fields.iter().find(|f| f.flatten),
allow_unknown_fields,
}
}
Expand All @@ -36,11 +38,29 @@ impl<'a> FieldsGen<'a> {
pub(in crate::codegen) fn core_loop(&self) -> TokenStream {
let arms = self.fields.as_ref().map(Field::as_match);

let (flatten_buffer, flatten_declaration) = if let Some(flatten_field) = self.flatten_field
{
(
quote! { let mut __flatten = vec![]; },
Some(flatten_field.as_flatten_initializer()),
)
} else {
(quote!(), None)
};

// If there is a flatten field, buffer the unknown field so it can be passed
// to the flatten function with all other unknown fields.
let handle_unknown = if self.flatten_field.is_some() {
quote! {
__flatten.push(__inner);
}
}
// If we're allowing unknown fields, then handling one is a no-op.
// Otherwise, we're going to push a new spanned error pointing at the field.
let handle_unknown = if self.allow_unknown_fields {
else if self.allow_unknown_fields {
quote!()
} else {
}
// Otherwise, we're going to push a new spanned error pointing at the field.
else {
let mut names = self.fields.iter().filter_map(Field::as_name).peekable();
// We can't call `unknown_field_with_alts` with an empty slice, or else it fails to
// infer the type of the slice item.
Expand All @@ -57,6 +77,8 @@ impl<'a> FieldsGen<'a> {
let arms = arms.iter();

quote!(
#flatten_buffer

for __item in __items {
match *__item {
::darling::export::NestedMeta::Meta(ref __inner) => {
Expand All @@ -72,6 +94,8 @@ impl<'a> FieldsGen<'a> {
}
}
}

#flatten_declaration
)
}

Expand Down
121 changes: 121 additions & 0 deletions tests/flatten.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use darling::{util::Flag, FromDeriveInput, FromMeta};
use proc_macro2::Ident;
use syn::parse_quote;

#[derive(FromMeta)]
struct Vis {
public: Flag,
private: Flag,
}

#[derive(FromDeriveInput)]
#[darling(attributes(sample))]
struct Example {
ident: Ident,
label: String,
#[darling(flatten)]
visibility: Vis,
}

#[test]
fn happy_path() {
let di = Example::from_derive_input(&parse_quote! {
#[sample(label = "Hello", public)]
struct Demo {}
});

let parsed = di.unwrap();
assert_eq!(parsed.ident, "Demo");
assert_eq!(&parsed.label, "Hello");
assert!(parsed.visibility.public.is_present());
assert!(!parsed.visibility.private.is_present());
}

#[test]
fn unknown_field_errors() {
let errors = Example::from_derive_input(&parse_quote! {
#[sample(label = "Hello", republic)]
struct Demo {}
})
.map(|_| "Should have failed")
.unwrap_err();

assert_eq!(errors.len(), 1);
}

/// This test demonstrates flatten being used recursively.
/// Fields are expected to be consumed by the outermost matching struct.
#[test]
fn recursive_flattening() {
#[derive(FromMeta)]
struct Nested2 {
above: isize,
below: isize,
port: Option<isize>,
}

#[derive(FromMeta)]
struct Nested1 {
port: isize,
starboard: isize,
#[darling(flatten)]
z_axis: Nested2,
}

#[derive(FromMeta)]
struct Nested0 {
fore: isize,
aft: isize,
#[darling(flatten)]
cross_section: Nested1,
}

#[derive(FromDeriveInput)]
#[darling(attributes(boat))]
struct BoatPosition {
#[darling(flatten)]
pos: Nested0,
}

let parsed = BoatPosition::from_derive_input(&parse_quote! {
#[boat(fore = 1, aft = 1, port = 10, starboard = 50, above = 20, below = -3)]
struct Demo;
})
.unwrap();

assert_eq!(parsed.pos.fore, 1);
assert_eq!(parsed.pos.aft, 1);

assert_eq!(parsed.pos.cross_section.port, 10);
assert_eq!(parsed.pos.cross_section.starboard, 50);

assert_eq!(parsed.pos.cross_section.z_axis.above, 20);
assert_eq!(parsed.pos.cross_section.z_axis.below, -3);
// This should be `None` because the `port` field in `Nested1` consumed
// the field before the leftovers were passed to `Nested2::from_list`.
assert_eq!(parsed.pos.cross_section.z_axis.port, None);
}

/// This test confirms that a collection - in this case a HashMap - can
/// be used with `flatten`.
#[test]
fn flattening_into_hashmap() {
#[derive(FromDeriveInput)]
#[darling(attributes(ca))]
struct Catchall {
hello: String,
volume: usize,
#[darling(flatten)]
others: std::collections::HashMap<String, String>,
}

let parsed = Catchall::from_derive_input(&parse_quote! {
#[ca(hello = "World", volume = 10, first_name = "Alice", second_name = "Bob")]
struct Demo;
})
.unwrap();

assert_eq!(parsed.hello, "World");
assert_eq!(parsed.volume, 10);
assert_eq!(parsed.others.len(), 2);
}
45 changes: 45 additions & 0 deletions tests/flatten_error_accumulation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use darling::{util::Flag, Error, FromDeriveInput, FromMeta};
use proc_macro2::Ident;
use syn::parse_quote;

#[derive(FromMeta)]
#[darling(and_then = Self::validate)]
struct Vis {
public: Flag,
private: Flag,
}

impl Vis {
fn validate(self) -> darling::Result<Self> {
if self.public.is_present() && self.private.is_present() {
return Err(Error::custom("Cannot be both public and private"));
}

Ok(self)
}
}

#[derive(FromDeriveInput)]
#[darling(attributes(sample))]
#[allow(dead_code)]
struct Example {
ident: Ident,
label: String,
volume: usize,
#[darling(flatten)]
visibility: Vis,
}

#[test]
fn many_errors() {
let e = Example::from_derive_input(&parse_quote! {
#[sample(volume = 10, public, private)]
struct Demo {}
})
.map(|_| "Should have failed")
.unwrap_err();

// We are expecting an error from the Vis::validate method and an error for the
// missing `label` field.
assert_eq!(e.len(), 2);
}

0 comments on commit 3eb3e93

Please sign in to comment.