mapstic_derive_impl/
lib.rs

1use std::collections::HashMap;
2
3use darling::{
4    ast::{Data, Fields},
5    FromDeriveInput, FromField,
6};
7use helpers::{FieldIdent, FieldsExt, StructType};
8use indexmap::IndexMap;
9use itertools::Itertools;
10use params::{AttrParams, AttrValue};
11use proc_macro2::{Span, TokenStream};
12use quote::{quote, ToTokens};
13use syn::{ext::IdentExt, DeriveInput, Ident, Type};
14
15mod error;
16mod helpers;
17mod params;
18
19pub use error::Error;
20
21/// Attributes defined on a top-level type that derives [`mapstic_core::ToMapping`].
22#[derive(FromDeriveInput, Debug)]
23#[darling(attributes(mapstic))]
24struct StructAttrs {
25    /// The optional `mapping_type` attribute, which prevents type inference from taking place.
26    #[darling(default)]
27    mapping_type: Option<String>,
28
29    /// The optional `params` attribute, used to define any field parameters that should be
30    /// included within the explicit mapping.
31    #[darling(default)]
32    params: HashMap<String, AttrValue>,
33
34    /// Fields defined within the type.
35    ///
36    /// Note that the enum variant of Data is defined as () — we simply won't be handling enums
37    /// unless `mapping_type` was defined.
38    data: Data<(), FieldAttrs>,
39}
40
41/// Attributes defined on a field within a struct deriving [`mapstic_core::ToMapping`].
42#[derive(FromField, Debug)]
43#[darling(attributes(mapstic))]
44struct FieldAttrs {
45    /// The field name.
46    ident: Option<Ident>,
47
48    /// The field type.
49    ty: Type,
50
51    /// The optional `mapping_type` attribute, which prevents type inference.
52    #[darling(default)]
53    mapping_type: Option<String>,
54
55    /// The optional `skip` attribute, which will result in the field being completely ignored.
56    #[darling(default)]
57    skip: bool,
58
59    /// The optional `params` attribute, used to define any field parameters that should be
60    /// included within the explicit mapping.
61    #[darling(default)]
62    params: HashMap<String, AttrValue>,
63}
64
65impl FieldIdent for FieldAttrs {
66    fn ident(&self) -> Option<&Ident> {
67        self.ident.as_ref()
68    }
69}
70
71/// Implementation of the [`mapstic_core::ToMapping`] derive macro.
72pub fn to_mapping(input: TokenStream) -> Result<TokenStream, Error> {
73    let input: DeriveInput = match syn::parse2(input) {
74        Ok(input) => input,
75        Err(e) => return Ok(e.to_compile_error()),
76    };
77
78    // Have darling parse the type definition.
79    let StructAttrs {
80        mapping_type,
81        params,
82        data,
83    } = match StructAttrs::from_derive_input(&input) {
84        Ok(attrs) => attrs,
85        Err(e) => return Ok(e.write_errors()),
86    };
87
88    // Get the bits of type metadata we need to build impl blocks later.
89    let DeriveInput {
90        generics, ident, ..
91    } = input;
92    let (impl_generics, ty_generics, where_generics) = generics.split_for_impl();
93
94    // Wrap the parameters to make them easier to use.
95    let params = AttrParams::from(params);
96
97    if let Some(ty) = mapping_type {
98        // If mapping_type was defined on the type, there's basically nothing for us to do here: we
99        // can simply generate a ToMapping impl based on the mapping type and whatever parameters
100        // were provided.
101        //
102        // Note that doing this before we check if we have a struct or enum means that this will
103        // also work for an enum that we otherwise wouldn't be able to handle.
104        Ok(quote! {
105            impl #impl_generics ::mapstic::ToMapping for #ident #ty_generics #where_generics {
106                fn to_mapping() -> ::mapstic::Mapping {
107                    ::mapstic::Mapping::scalar(#ty, #params)
108                }
109
110                fn to_mapping_with_params(params: ::mapstic::Params) -> ::mapstic::Mapping {
111                    // Extend the parameters from the struct, if any, with the parameters passed in
112                    // from any field definitions that use this struct.
113                    let mut local = #params;
114                    local.extend(params);
115
116                    ::mapstic::Mapping::scalar(#ty, local)
117                }
118            }
119        })
120    } else {
121        // OK, so no mapping_type. Let's see what we have in this type.
122        let repr = match data {
123            Data::Struct(fields) => TypeRepr::try_from_fields(fields, params.iter())?,
124            // We can't automatically derive enums, so let's just bail and give the user a
125            // hopefully helpful message.
126            Data::Enum(_) => return Err(Error::Enum),
127        };
128
129        match repr {
130            // This is a tuple struct with one field: we should treat this as if it has been
131            // flattened into its container.
132            TypeRepr::Flat(mut field) => {
133                field.params.extend(params);
134
135                // This is a little hacky: while the generated code using #field would include any
136                // parameters defined on the tuple struct or the tuple struct's field, it wouldn't
137                // include any parameters passed in from a container unless they're added to the
138                // parameters at runtime in to_mapping_with_params, but the ToTokens impl for Field
139                // doesn't take that into account. Instead, we'll use FieldWithInjectedParams to
140                // extend the parameters from a variable in the environment.
141                let inj = FieldWithInjectedParams {
142                    field: &field,
143                    ident: Ident::new("params", Span::call_site()),
144                };
145
146                Ok(quote! {
147                    impl #impl_generics ::mapstic::ToMapping for #ident #ty_generics #where_generics {
148                        fn to_mapping() -> ::mapstic::Mapping {
149                            #field
150                        }
151
152                        fn to_mapping_with_params(mut params: ::mapstic::Params) -> ::mapstic::Mapping {
153                            #inj
154                        }
155                    }
156                })
157            }
158            // A traditional named struct with one or more fields.
159            TypeRepr::Named(fields) => {
160                let param_extend = match params.is_empty() {
161                    false => Some(quote! { params.extend(#params); }),
162                    true => None,
163                };
164
165                Ok(quote! {
166                    impl #impl_generics ::mapstic::ToMapping for #ident #ty_generics #where_generics {
167                        fn to_mapping() -> ::mapstic::Mapping {
168                            ::mapstic::Mapping::object(#fields.into_iter(), #params)
169                        }
170
171                        fn to_mapping_with_params(mut params: ::mapstic::Params) -> ::mapstic::Mapping {
172                            #param_extend
173                            ::mapstic::Mapping::object(#fields.into_iter(), params)
174                        }
175                    }
176                })
177            }
178        }
179    }
180}
181
182/// A collection of named fields.
183struct NamedFields(IndexMap<String, Field>);
184
185impl ToTokens for NamedFields {
186    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
187        // This is represented as a slice of (name, value) pairs.
188
189        let fields = self.0.iter().map(|(name, field)| {
190            quote! { (#name, #field) }
191        });
192
193        tokens.extend(quote! {
194            [
195                #( #fields ),*
196            ]
197        });
198    }
199}
200
201/// A field, which includes both a type and an optional set of parameters.
202#[derive(Debug)]
203struct Field {
204    ty: FieldType,
205    params: AttrParams,
206}
207
208impl From<FieldAttrs> for Field {
209    fn from(opts: FieldAttrs) -> Self {
210        Self {
211            ty: match opts.mapping_type {
212                Some(ty) => FieldType::Explicit(ty),
213                None => FieldType::Inferred(opts.ty.into_token_stream()),
214            },
215            params: opts.params.into(),
216        }
217    }
218}
219
220impl ToTokens for Field {
221    fn to_tokens(&self, tokens: &mut TokenStream) {
222        // This is represented as an expression that returns a mapstic::Mapping.
223
224        let Field { ty, params } = self;
225
226        tokens.extend(match ty {
227            FieldType::Explicit(ty) => quote! {
228                ::mapstic::Mapping::scalar(#ty, #params)
229            },
230            FieldType::Inferred(ty) => {
231                quote! { <#ty as ::mapstic::ToMapping>::to_mapping_with_params(#params.into()) }
232            }
233        });
234    }
235}
236
237/// An extension to [`Field`] which allows for parameters to be injected from the environment when
238/// building the tokens that return a [`mapstic_core::Mapping`].
239///
240/// It is up to the caller to ensure that [`ToTokens`] is used in a context where the [`Ident`]
241/// variable is available.
242struct FieldWithInjectedParams<'a> {
243    field: &'a Field,
244    ident: Ident,
245}
246
247impl ToTokens for FieldWithInjectedParams<'_> {
248    fn to_tokens(&self, tokens: &mut TokenStream) {
249        // Like the Field impl of ToTokens, this is represented as an expression that returns a
250        // mapstic::Mapping, but with the added wrinkle that the parameters will be merged with a
251        // variable that is required to be defined outside of the context within which this
252        // function is called.
253
254        let Self {
255            field: Field { ty, params },
256            ident,
257        } = self;
258
259        let params = quote! {
260            let mut local = #params;
261            local.extend(#ident);
262        };
263
264        tokens.extend(match ty {
265            FieldType::Explicit(ty) => quote! {{
266                #params
267                ::mapstic::Mapping::scalar(#ty, local)
268            }},
269            FieldType::Inferred(ty) => quote! {{
270                #params
271                <#ty as ::mapstic::ToMapping>::to_mapping_with_params(local)
272            }},
273        });
274    }
275}
276
277/// The type of a [`Field`]: either an explicit string type that Elasticsearch understands, or a
278/// Rust type (represented as a [`TokenStream`]) that impls [`mapstic_core::ToMapping`].
279#[derive(Debug)]
280enum FieldType {
281    Explicit(String),
282    Inferred(TokenStream),
283}
284
285/// The representation of a type deriving [`mapstic_core::ToMapping`]: either as a flat type, or a
286/// set of named fields.
287enum TypeRepr {
288    Flat(Field),
289    Named(NamedFields),
290}
291
292impl TypeRepr {
293    /// Given a set of fields parsed from a `struct`, figure out the right representation of the
294    /// fields.
295    fn try_from_fields<'a, I, K>(
296        fields: Fields<FieldAttrs>,
297        container_params: I,
298    ) -> Result<Self, Error>
299    where
300        I: Iterator<Item = (K, &'a AttrValue)>,
301        K: ToString,
302    {
303        match fields.into_struct_type() {
304            StructType::Unit => Err(Error::UnitStruct),
305            StructType::TupleMultiple(_) => Err(Error::TupleStruct),
306            StructType::TupleSingle(opts) if opts.skip => Err(Error::AllSkipped),
307            StructType::TupleSingle(opts) => {
308                // Special case: if there are parameters on the container attribute, we need to
309                // merge these into the field parameters, since this is being flattened.
310                let mut field = Field::from(opts);
311                field
312                    .params
313                    .extend(container_params.map(|(k, v)| (k.to_string(), v.clone())));
314
315                Ok(Self::Flat(field))
316            }
317            StructType::Named(fields) => {
318                let types: IndexMap<_, _> = fields
319                    .into_iter()
320                    .filter(|opts| !opts.skip)
321                    .map(|opts| match opts.ident {
322                        Some(ref ident) => Ok((ident.unraw().to_string(), Field::from(opts))),
323                        None => Err(Error::MixedStruct),
324                    })
325                    .try_collect()?;
326
327                if types.is_empty() {
328                    Err(Error::AllSkipped)
329                } else {
330                    Ok(Self::Named(NamedFields(types)))
331                }
332            }
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use std::str::FromStr;
340
341    use claims::assert_matches;
342
343    use super::*;
344
345    #[test]
346    fn all_skipped_named() {
347        let source = r#"
348            #[derive(ToMapping)]
349            struct Foo {
350                #[mapstic(skip)]
351                a: String,
352
353                #[mapstic(skip)]
354                b: String,
355            }
356        "#;
357        let stream = TokenStream::from_str(source).expect("parse input");
358
359        assert_matches!(to_mapping(stream), Err(Error::AllSkipped));
360    }
361
362    #[test]
363    fn all_skipped_tuple() {
364        let source = r#"
365            #[derive(ToMapping)]
366            struct Foo(#[mapstic(skip)] String);
367        "#;
368        let stream = TokenStream::from_str(source).expect("parse input");
369
370        assert_matches!(to_mapping(stream), Err(Error::AllSkipped));
371    }
372
373    #[test]
374    fn enum_fail() {
375        let source = r#"
376            #[derive(ToMapping)]
377            enum E { A, B }
378        "#;
379        let stream = TokenStream::from_str(source).expect("parse input");
380
381        assert_matches!(to_mapping(stream), Err(Error::Enum));
382    }
383
384    #[test]
385    fn tuple() {
386        let source = r#"
387            #[derive(ToMapping)]
388            struct Foo(String, String);
389        "#;
390        let stream = TokenStream::from_str(source).expect("parse input");
391
392        assert_matches!(to_mapping(stream), Err(Error::TupleStruct));
393    }
394
395    #[test]
396    fn unit() {
397        let source = r#"
398            #[derive(ToMapping)]
399            struct Foo;
400        "#;
401        let stream = TokenStream::from_str(source).expect("parse input");
402
403        assert_matches!(to_mapping(stream), Err(Error::UnitStruct));
404    }
405}