diff --git a/CHANGELOG.md b/CHANGELOG.md index d351fa0..38ffa3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Re-export `syn` from `darling` to avoid requiring that consuming crates have a `syn` dependency. - Change ` as FromMeta>` impl to more precisely capture the _value_ span, as opposed to the span of the entire item. +- Add `darling::util::{AsShape, Shape, ShapeSet}` to improve "shape" validation for structs and variants. [#222](https://github.com/TedDriggs/issues/222) ## v0.14.2 (October 26, 2022) diff --git a/core/src/codegen/from_derive_impl.rs b/core/src/codegen/from_derive_impl.rs index 5b8477d..1c1df63 100644 --- a/core/src/codegen/from_derive_impl.rs +++ b/core/src/codegen/from_derive_impl.rs @@ -5,7 +5,7 @@ use syn::Ident; use crate::{ ast::Data, codegen::{ExtractAttribute, OuterFromImpl, TraitImpl}, - options::{ForwardAttrs, Shape}, + options::{DeriveInputShapeSet, ForwardAttrs}, util::PathList, }; @@ -19,7 +19,7 @@ pub struct FromDeriveInputImpl<'a> { pub attr_names: &'a PathList, pub forward_attrs: Option<&'a ForwardAttrs>, pub from_ident: bool, - pub supports: Option<&'a Shape>, + pub supports: Option<&'a DeriveInputShapeSet>, } impl<'a> ToTokens for FromDeriveInputImpl<'a> { diff --git a/core/src/codegen/from_variant_impl.rs b/core/src/codegen/from_variant_impl.rs index 99674b0..1598e6f 100644 --- a/core/src/codegen/from_variant_impl.rs +++ b/core/src/codegen/from_variant_impl.rs @@ -65,8 +65,7 @@ impl<'a> ToTokens for FromVariantImpl<'a> { let supports = self.supports.map(|i| { quote! { - #i - __errors.handle(__validate_data(&#input.fields)); + __errors.handle(#i.check(&#input.fields)); } }); diff --git a/core/src/options/from_derive.rs b/core/src/options/from_derive.rs index 5507451..0f1e1c4 100644 --- a/core/src/options/from_derive.rs +++ b/core/src/options/from_derive.rs @@ -3,7 +3,7 @@ use quote::ToTokens; use syn::Ident; use crate::codegen::FromDeriveInputImpl; -use crate::options::{OuterFrom, ParseAttribute, ParseData, Shape}; +use crate::options::{DeriveInputShapeSet, OuterFrom, ParseAttribute, ParseData}; use crate::{FromMeta, Result}; #[derive(Debug)] @@ -18,7 +18,7 @@ pub struct FdiOptions { pub data: Option, - pub supports: Option, + pub supports: Option, } impl FdiOptions { diff --git a/core/src/options/mod.rs b/core/src/options/mod.rs index 4ca2a59..d8a147d 100644 --- a/core/src/options/mod.rs +++ b/core/src/options/mod.rs @@ -27,7 +27,7 @@ pub use self::from_variant::FromVariantOptions; pub use self::input_field::InputField; pub use self::input_variant::InputVariant; pub use self::outer_from::OuterFrom; -pub use self::shape::{DataShape, Shape}; +pub use self::shape::{DataShape, DeriveInputShapeSet}; /// A default/fallback expression encountered in attributes during parsing. #[derive(Debug, Clone)] diff --git a/core/src/options/shape.rs b/core/src/options/shape.rs index 0edf478..5245a00 100644 --- a/core/src/options/shape.rs +++ b/core/src/options/shape.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{Meta, NestedMeta}; +use syn::{parse_quote, Meta, NestedMeta}; use crate::{Error, FromMeta, Result}; @@ -11,24 +11,19 @@ use crate::{Error, FromMeta, Result}; /// to declare that it only accepts - for example - named structs, or newtype enum /// variants. /// -/// # Usage -/// Because `Shape` implements `FromMeta`, the name of the field where it appears is -/// controlled by the struct that declares `Shape` as a member. That field name is -/// shown as `ignore` below. -/// /// ```rust,ignore /// #[ignore(any, struct_named, enum_newtype)] /// ``` #[derive(Debug, Clone)] -pub struct Shape { +pub struct DeriveInputShapeSet { enum_values: DataShape, struct_values: DataShape, any: bool, } -impl Default for Shape { +impl Default for DeriveInputShapeSet { fn default() -> Self { - Shape { + DeriveInputShapeSet { enum_values: DataShape::new("enum_"), struct_values: DataShape::new("struct_"), any: Default::default(), @@ -36,9 +31,9 @@ impl Default for Shape { } } -impl FromMeta for Shape { +impl FromMeta for DeriveInputShapeSet { fn from_list(items: &[NestedMeta]) -> Result { - let mut new = Shape::default(); + let mut new = DeriveInputShapeSet::default(); for item in items { if let NestedMeta::Meta(Meta::Path(ref path)) = *item { let ident = &path.segments.first().unwrap().ident; @@ -65,7 +60,7 @@ impl FromMeta for Shape { } } -impl ToTokens for Shape { +impl ToTokens for DeriveInputShapeSet { fn to_tokens(&self, tokens: &mut TokenStream) { let fn_body = if self.any { quote!(::darling::export::Ok(())) @@ -73,33 +68,37 @@ impl ToTokens for Shape { let en = &self.enum_values; let st = &self.struct_values; - let enum_validation = if en.supports_none() { - let ty = en.prefix.trim_end_matches('_'); - quote!(return ::darling::export::Err(::darling::Error::unsupported_shape(#ty));) - } else { - quote! { - fn validate_variant(data: &::darling::export::syn::Fields) -> ::darling::Result<()> { - #en - } + quote! { + { + let struct_check = #st; + let enum_check = #en; - for variant in &data.variants { - validate_variant(&variant.fields)?; - } + match *__body { + ::darling::export::syn::Data::Enum(ref data) => { + if enum_check.is_empty() { + return ::darling::export::Err( + ::darling::Error::unsupported_shape_with_expected("enum", &format!("struct with {}", struct_check)) + ); + } - Ok(()) - } - }; + let mut variant_errors = ::darling::Error::accumulator(); + for variant in &data.variants { + variant_errors.handle(enum_check.check(variant)); + } - quote! { - match *__body { - ::darling::export::syn::Data::Enum(ref data) => { - #enum_validation - } - ::darling::export::syn::Data::Struct(ref struct_data) => { - let data = &struct_data.fields; - #st + variant_errors.finish() + } + ::darling::export::syn::Data::Struct(ref struct_data) => { + if struct_check.is_empty() { + return ::darling::export::Err( + ::darling::Error::unsupported_shape_with_expected("struct", &format!("enum with {}", enum_check)) + ); + } + + struct_check.check(struct_data) + } + ::darling::export::syn::Data::Union(_) => unreachable!(), } - ::darling::export::syn::Data::Union(_) => unreachable!(), } } }; @@ -124,25 +123,16 @@ pub struct DataShape { tuple: bool, unit: bool, any: bool, - /// Control whether the emitted code should be inside a function or not. - /// This is `true` when creating a `Shape` for `FromDeriveInput`, but false - /// when deriving `FromVariant`. - embedded: bool, } impl DataShape { fn new(prefix: &'static str) -> Self { DataShape { prefix, - embedded: true, ..Default::default() } } - fn supports_none(&self) -> bool { - !(self.any || self.newtype || self.named || self.tuple || self.unit) - } - fn set_word(&mut self, word: &str) -> Result<()> { match word.trim_start_matches(self.prefix) { "newtype" => { @@ -189,43 +179,37 @@ impl FromMeta for DataShape { impl ToTokens for DataShape { fn to_tokens(&self, tokens: &mut TokenStream) { - let body = if self.any { - quote!(::darling::export::Ok(())) - } else if self.supports_none() { - let ty = self.prefix.trim_end_matches('_'); - quote!(::darling::export::Err(::darling::Error::unsupported_shape(#ty))) - } else { - let unit = match_arm("unit", self.unit); - let newtype = match_arm("newtype", self.newtype); - let named = match_arm("named", self.named); - let tuple = match_arm("tuple", self.tuple); - quote! { - match *data { - ::darling::export::syn::Fields::Unit => #unit, - ::darling::export::syn::Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => #newtype, - ::darling::export::syn::Fields::Unnamed(_) => #tuple, - ::darling::export::syn::Fields::Named(_) => #named, - } - } - }; + let Self { + any, + named, + tuple, + unit, + newtype, + .. + } = *self; - if self.embedded { - body.to_tokens(tokens); - } else { - tokens.append_all(quote! { - fn __validate_data(data: &::darling::export::syn::Fields) -> ::darling::Result<()> { - #body - } - }); + let shape_path: syn::Path = parse_quote!(::darling::util::Shape); + + let mut shapes = vec![]; + if any || named { + shapes.push(quote!(#shape_path::Named)); } - } -} -fn match_arm(name: &'static str, is_supported: bool) -> TokenStream { - if is_supported { - quote!(::darling::export::Ok(())) - } else { - quote!(::darling::export::Err(::darling::Error::unsupported_shape(#name))) + if any || tuple { + shapes.push(quote!(#shape_path::Tuple)); + } + + if any || newtype { + shapes.push(quote!(#shape_path::Newtype)); + } + + if any || unit { + shapes.push(quote!(#shape_path::Unit)); + } + + tokens.append_all(quote! { + ::darling::util::ShapeSet::new(vec![#(#shapes),*]) + }); } } @@ -235,7 +219,7 @@ mod tests { use quote::quote; use syn::parse_quote; - use super::Shape; + use super::DeriveInputShapeSet; use crate::FromMeta; /// parse a string as a syn::Meta instance. @@ -251,20 +235,21 @@ mod tests { #[test] fn supports_any() { - let decl = fm::(quote!(ignore(any))); + let decl = fm::(quote!(ignore(any))); assert!(decl.any); } #[test] fn supports_struct() { - let decl = fm::(quote!(ignore(struct_any, struct_newtype))); + let decl = fm::(quote!(ignore(struct_any, struct_newtype))); assert!(decl.struct_values.any); assert!(decl.struct_values.newtype); } #[test] fn supports_mixed() { - let decl = fm::(quote!(ignore(struct_newtype, enum_newtype, enum_tuple))); + let decl = + fm::(quote!(ignore(struct_newtype, enum_newtype, enum_tuple))); assert!(decl.struct_values.newtype); assert!(decl.enum_values.newtype); assert!(decl.enum_values.tuple);