darling_core/options/
shape.rs

1//! Types for "shape" validation. This allows types deriving `FromDeriveInput` etc. to declare
2//! that they only work on - for example - structs with named fields, or newtype enum variants.
3
4use proc_macro2::{Span, TokenStream};
5use quote::{quote, ToTokens, TokenStreamExt};
6use syn::{parse_quote, Meta};
7
8use crate::ast::NestedMeta;
9use crate::{Error, FromMeta, Result};
10
11/// Receiver struct for shape validation. Shape validation allows a deriving type
12/// to declare that it only accepts - for example - named structs, or newtype enum
13/// variants.
14///
15/// ```rust,ignore
16/// #[ignore(any, struct_named, enum_newtype)]
17/// ```
18#[derive(Debug, Clone)]
19pub struct DeriveInputShapeSet {
20    enum_values: DataShape,
21    struct_values: DataShape,
22    any: bool,
23}
24
25impl DeriveInputShapeSet {
26    fn validator_fn_ident(&self) -> syn::Ident {
27        syn::Ident::new("__validate_body", Span::call_site())
28    }
29
30    pub fn validator_path(&self) -> syn::Path {
31        if self.any {
32            parse_quote!(::darling::export::Ok)
33        } else {
34            self.validator_fn_ident().into()
35        }
36    }
37}
38
39impl Default for DeriveInputShapeSet {
40    fn default() -> Self {
41        DeriveInputShapeSet {
42            enum_values: DataShape::new("enum_"),
43            struct_values: DataShape::new("struct_"),
44            any: Default::default(),
45        }
46    }
47}
48
49impl FromMeta for DeriveInputShapeSet {
50    fn from_list(items: &[NestedMeta]) -> Result<Self> {
51        let mut new = DeriveInputShapeSet::default();
52        for item in items {
53            if let NestedMeta::Meta(Meta::Path(ref path)) = *item {
54                let ident = &path.segments.first().unwrap().ident;
55                let word = ident.to_string();
56                if word == "any" {
57                    new.any = true;
58                } else if word.starts_with("enum_") {
59                    new.enum_values
60                        .set_word(&word)
61                        .map_err(|e| e.with_span(&ident))?;
62                } else if word.starts_with("struct_") {
63                    new.struct_values
64                        .set_word(&word)
65                        .map_err(|e| e.with_span(&ident))?;
66                } else {
67                    return Err(Error::unknown_value(&word).with_span(&ident));
68                }
69            } else {
70                return Err(Error::unsupported_format("non-word").with_span(item));
71            }
72        }
73
74        Ok(new)
75    }
76}
77
78/// Generates a body-shape validator if and only if that validation could fail for some inputs.
79impl ToTokens for DeriveInputShapeSet {
80    fn to_tokens(&self, tokens: &mut TokenStream) {
81        if self.any {
82            return;
83        }
84
85        let fn_body = {
86            let en = &self.enum_values;
87            let st = &self.struct_values;
88
89            quote! {
90                {
91                    let struct_check = #st;
92                    let enum_check = #en;
93
94                    match *__body {
95                        ::darling::export::syn::Data::Enum(ref data) => {
96                            if enum_check.is_empty() {
97                                return ::darling::export::Err(
98                                    ::darling::Error::unsupported_shape_with_expected("enum", &format!("struct with {}", struct_check))
99                                );
100                            }
101
102                            let mut variant_errors = ::darling::Error::accumulator();
103                            for variant in &data.variants {
104                                variant_errors.handle(enum_check.check(variant));
105                            }
106
107                            variant_errors.finish_with(__body)
108                        }
109                        ::darling::export::syn::Data::Struct(ref struct_data) => {
110                            if struct_check.is_empty() {
111                                return ::darling::export::Err(
112                                    ::darling::Error::unsupported_shape_with_expected("struct", &format!("enum with {}", enum_check))
113                                );
114                            }
115
116                            struct_check.check(struct_data).and(::darling::export::Ok(__body))
117                        }
118                        ::darling::export::syn::Data::Union(_) => {
119                            let expected = if enum_check.is_empty() {
120                                format!("struct with {}", struct_check)
121                            } else if struct_check.is_empty() {
122                                format!("enum with {}", enum_check)
123                            } else {
124                                format!("struct with {} or enum with {}", struct_check, enum_check)
125                            };
126                            ::darling::export::Err(::darling::Error::unsupported_shape_with_expected("union", &expected))
127                        },
128                    }
129                }
130            }
131        };
132
133        let fn_ident = self.validator_fn_ident();
134
135        tokens.append_all(quote! {
136            fn #fn_ident(__body: &::darling::export::syn::Data) -> ::darling::Result<&::darling::export::syn::Data> {
137                #fn_body
138            }
139        });
140    }
141}
142
143/// Receiver for shape information within a struct or enum context. See `Shape` for more information
144/// on valid uses of shape validation.
145#[derive(Debug, Clone, Default, PartialEq, Eq)]
146pub struct DataShape {
147    /// The kind of shape being described. This can be `struct_` or `enum_`.
148    prefix: &'static str,
149    newtype: bool,
150    named: bool,
151    tuple: bool,
152    unit: bool,
153    any: bool,
154}
155
156impl DataShape {
157    fn new(prefix: &'static str) -> Self {
158        DataShape {
159            prefix,
160            ..Default::default()
161        }
162    }
163
164    fn set_word(&mut self, word: &str) -> Result<()> {
165        match word.trim_start_matches(self.prefix) {
166            "newtype" => {
167                self.newtype = true;
168                Ok(())
169            }
170            "named" => {
171                self.named = true;
172                Ok(())
173            }
174            "tuple" => {
175                self.tuple = true;
176                Ok(())
177            }
178            "unit" => {
179                self.unit = true;
180                Ok(())
181            }
182            "any" => {
183                self.any = true;
184                Ok(())
185            }
186            _ => Err(Error::unknown_value(word)),
187        }
188    }
189}
190
191impl FromMeta for DataShape {
192    fn from_list(items: &[NestedMeta]) -> Result<Self> {
193        let mut errors = Error::accumulator();
194        let mut new = DataShape::default();
195
196        for item in items {
197            if let NestedMeta::Meta(Meta::Path(ref path)) = *item {
198                errors.handle(new.set_word(&path.segments.first().unwrap().ident.to_string()));
199            } else {
200                errors.push(Error::unsupported_format("non-word").with_span(item));
201            }
202        }
203
204        errors.finish_with(new)
205    }
206}
207
208impl ToTokens for DataShape {
209    fn to_tokens(&self, tokens: &mut TokenStream) {
210        let Self {
211            any,
212            named,
213            tuple,
214            unit,
215            newtype,
216            ..
217        } = *self;
218
219        let shape_path: syn::Path = parse_quote!(::darling::util::Shape);
220
221        let mut shapes = vec![];
222        if any || named {
223            shapes.push(quote!(#shape_path::Named));
224        }
225
226        if any || tuple {
227            shapes.push(quote!(#shape_path::Tuple));
228        }
229
230        if any || newtype {
231            shapes.push(quote!(#shape_path::Newtype));
232        }
233
234        if any || unit {
235            shapes.push(quote!(#shape_path::Unit));
236        }
237
238        tokens.append_all(quote! {
239            ::darling::util::ShapeSet::new(vec![#(#shapes),*])
240        });
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use proc_macro2::TokenStream;
247    use quote::quote;
248    use syn::parse_quote;
249
250    use super::DeriveInputShapeSet;
251    use crate::FromMeta;
252
253    /// parse a string as a syn::Meta instance.
254    fn pm(tokens: TokenStream) -> ::std::result::Result<syn::Meta, String> {
255        let attribute: syn::Attribute = parse_quote!(#[#tokens]);
256        Ok(attribute.meta)
257    }
258
259    fn fm<T: FromMeta>(tokens: TokenStream) -> T {
260        FromMeta::from_meta(&pm(tokens).expect("Tests should pass well-formed input"))
261            .expect("Tests should pass valid input")
262    }
263
264    #[test]
265    fn supports_any() {
266        let decl = fm::<DeriveInputShapeSet>(quote!(ignore(any)));
267        assert!(decl.any);
268    }
269
270    #[test]
271    fn supports_struct() {
272        let decl = fm::<DeriveInputShapeSet>(quote!(ignore(struct_any, struct_newtype)));
273        assert!(decl.struct_values.any);
274        assert!(decl.struct_values.newtype);
275    }
276
277    #[test]
278    fn supports_mixed() {
279        let decl =
280            fm::<DeriveInputShapeSet>(quote!(ignore(struct_newtype, enum_newtype, enum_tuple)));
281        assert!(decl.struct_values.newtype);
282        assert!(decl.enum_values.newtype);
283        assert!(decl.enum_values.tuple);
284        assert!(!decl.struct_values.any);
285    }
286}