descriptor_derive/
lib.rs

1use convert_case::{Case, Casing};
2use proc_macro2::TokenStream;
3use proc_macro_error::{abort, proc_macro_error};
4use quote::quote;
5use syn::{parse_macro_input, Fields, Ident, Item, ItemEnum, ItemStruct, Type, TypePath};
6
7use crate::parse::{DescriptorFieldAttr, DescriptorStructAttr};
8
9mod parse;
10
11#[derive(Clone)]
12struct StructField {
13    ident: Ident,
14    typ: Type,
15    field_name: String,
16    attr: DescriptorFieldAttr,
17}
18
19#[proc_macro_derive(Descriptor, attributes(descriptor))]
20#[proc_macro_error]
21pub fn descriptor(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
22    let input = parse_macro_input!(input as Item);
23    match input {
24        Item::Enum(item) => generate_enum_decriptor(item),
25        Item::Struct(item) => generate_struct_decriptor(item),
26        _ => abort! {input, "not implemented for kind of input"},
27    }
28}
29
30/// The purpose of the function is to implement the decriptor trait automatically
31/// User can provide some attribute in order to customize the Description/Table generated
32/// See parse.rs to check all possible attributes
33/// The decriptor trait has 6 method:
34/// `to_field` is the final call to have a String result of a field
35///     We generate a `match` on field_name wanted, and forward it to inner struct if a dot exist
36/// `default_headers` will return all headers by default, or just the one provided by the user
37/// `headers` will generate the list of header recursively
38/// `header_name` a method to get an header overrided name
39/// `struct_pad` internal method in order to get the padding to apply for fields
40/// `describe` will call describe on all fields in order to decriptor the description.
41/// ```
42fn generate_struct_decriptor(input: ItemStruct) -> proc_macro::TokenStream {
43    let name = &input.ident;
44
45    let decriptor_struct_attributes = parse::extract_struct_attributes(&input.attrs);
46    let fields = extract_field(&input);
47
48    let describe = describe_method_for_struct(&fields, &decriptor_struct_attributes);
49    let default_headers = default_headers_for_struct(&fields, &decriptor_struct_attributes);
50    let headers = headers_for_struct(&fields, &decriptor_struct_attributes);
51    let header_name_func = rename_headers_for_struct(&fields, &decriptor_struct_attributes);
52    let to_field = to_field_for_struct(&fields, &decriptor_struct_attributes);
53    let pad_struct = pad_struct(&fields);
54
55    generate_trait(
56        name,
57        describe,
58        to_field,
59        Some(pad_struct),
60        Some(default_headers),
61        Some(headers),
62        Some(header_name_func),
63    )
64    .into()
65}
66
67fn pad_struct(fields: &[StructField]) -> TokenStream {
68    let pad = match fields
69        .iter()
70        .map(|field| field.field_name.to_case(Case::Title).len())
71        .max()
72    {
73        None => 0,
74        Some(x) => x + 1,
75    };
76
77    let mut max_pad = quote! {
78        let pad = #pad;
79    };
80
81    for field in fields {
82        if field.attr.flatten {
83            let typ = &field.typ;
84            max_pad.extend(quote! {
85                let pad = pad.max(#typ::struct_pad());
86            })
87        }
88    }
89
90    quote! {
91        #max_pad
92        pad
93    }
94}
95
96fn extract_field(input: &ItemStruct) -> Vec<StructField> {
97    match &input.fields {
98        Fields::Named(named) => named
99            .named
100            .iter()
101            .map(|field| {
102                let ident = match field.ident.as_ref() {
103                    None => abort! {input.ident, "no identifier on field"},
104                    Some(ident) => ident,
105                };
106
107                StructField {
108                    ident: ident.clone(),
109                    typ: field.ty.clone(),
110                    field_name: ident.to_string(),
111                    attr: parse::extract_field_attributes(&field.attrs),
112                }
113            })
114            .filter(|x| !x.attr.skip)
115            .collect::<Vec<StructField>>(),
116        _ => abort! {input.ident, "not implemented for unnamed struct"},
117    }
118}
119
120// Generate the to_field method implementation for the struct
121fn to_field_for_struct(fields: &[StructField], struct_attributes: &DescriptorStructAttr) -> TokenStream {
122    let mut match_to_field = quote!();
123
124    fields
125        .iter()
126        .map(|field| {
127            let field_name = &field.field_name;
128
129            let value = field_getter(
130                &field,
131                quote! {
132                    to_field(_child)
133                },
134            );
135
136            quote! {
137                #field_name => {#value},
138            }
139        })
140        .for_each(|ts| match_to_field.extend(ts));
141
142    let fallback = if let Some(extra_fields) = &struct_attributes.extra_fields {
143        quote! {
144            _ => {
145                Into::<#extra_fields>::into(self).to_field(field_name)
146            },
147        }
148    } else {
149        quote! {
150            _ => "field not found".to_string(),
151        }
152    };
153
154    let return_value = if let Some(map) = &struct_attributes.map {
155        quote! {
156            #map(&self, value)
157        }
158    } else {
159        quote! {
160            value
161        }
162    };
163
164    let func = quote! {
165        let (field, _child) = descriptor::get_keys(field_name);
166
167        let value = match field {
168            #match_to_field
169            #fallback
170        };
171
172        #return_value
173    };
174
175    func
176}
177
178// Generate the rename_header method implementation for the struct
179fn rename_headers_for_struct(
180    fields: &[StructField],
181    struct_attributes: &DescriptorStructAttr,
182) -> TokenStream {
183    let mut rename_headers = quote!();
184
185    fields
186        .iter()
187        .map(|field| {
188            let typ = &field.typ;
189            let field_name = &field.field_name;
190
191            match &field.attr.rename_header {
192                Some(rename) => quote! {
193                    #field_name => Some(#rename.to_string()),
194                },
195                None => {
196                    if let Some(into) = &field.attr.into {
197                        quote! {
198                            #field_name => <#into>::header_name(_child),
199                        }
200                    } else {
201                        quote! {
202                            #field_name => <#typ>::header_name(_child),
203                        }
204                    }
205                }
206            }
207        })
208        .for_each(|ts| rename_headers.extend(ts));
209
210    if let Some(extra_fields) = &struct_attributes.extra_fields {
211        rename_headers.extend(quote! {
212            stringify!(#extra_fields) => <#extra_fields>::header_name(_child),
213        });
214    }
215
216    let func = quote! {
217        let (header, _child) = descriptor::get_keys(header);
218        match header {
219            #rename_headers
220            _ => None,
221        }
222    };
223    func
224}
225
226// Will generate the header function, we list all possible fields recursively
227fn headers_for_struct(fields: &[StructField], struct_attributes: &DescriptorStructAttr) -> TokenStream {
228    let mut headers = quote! {
229        let mut headers = Vec::new();
230    };
231
232    for field in fields.iter() {
233        let typ = &field.typ;
234        let field_name = &field.field_name;
235
236        headers.extend(if let Some(into) = &field.attr.into {
237            quote! {
238                let mut fields = <#into>::default_headers()
239            }
240        } else {
241            quote! {
242                let mut fields = <#typ>::default_headers()
243            }
244        });
245
246        headers.extend(quote! {
247                .into_iter()
248                .map(|x| format!("{}.{}", #field_name, x).to_string())
249                .collect::<Vec<String>>();
250
251                if fields.is_empty() {
252                    headers.push(#field_name.to_string());
253                } else {
254                    headers.append(&mut fields);
255                }
256        });
257    }
258
259    if let Some(extra_fields) = &struct_attributes.extra_fields {
260        headers.extend(quote! {
261            let mut fields = <#extra_fields>::default_headers();
262            headers.append(&mut fields);
263        })
264    }
265
266    headers.extend(quote! {
267        // return headers at end of function
268        headers
269    });
270
271    headers
272}
273
274// Generate the default_headers method implementation for the struct
275fn default_headers_for_struct(
276    fields: &[StructField],
277    struct_attributes: &DescriptorStructAttr,
278) -> TokenStream {
279    match struct_attributes.headers.as_ref() {
280        Some(headers) => {
281            quote! {
282                #headers.iter().map(|x| x.to_string()).collect::<Vec<String>>()
283            }
284        }
285        None => {
286            let mut slice = quote! {};
287
288            fields
289                .iter()
290                .filter(|x| x.attr.skip_header)
291                .map(|x| x.field_name.to_string())
292                .for_each(|x| {
293                    slice.extend(quote! {
294                        #x,
295                    })
296                });
297
298            quote! {
299                const SKIP : &'static[&'static str] = &[#slice];
300                Self::headers()
301                    .into_iter()
302                    .filter(|x| !SKIP.contains(&x.as_str()))
303                    .map(|x|x.to_string())
304                    .collect::<Vec<_>>()
305            }
306        }
307    }
308}
309
310// Generate the describe method implementation for the struct
311fn describe_method_for_struct(
312    fields: &[StructField],
313    struct_attributes: &DescriptorStructAttr,
314) -> TokenStream {
315    match struct_attributes.into.as_ref() {
316        Some(into) => {
317            quote! {
318                Into::<#into>::into(self).describe(writer, ctx.clone())
319            }
320        }
321        None => {
322            let mut describe = quote!();
323
324            fields
325                .iter()
326                .filter(|x| !x.attr.skip_description)
327                .enumerate()
328                .map(|(i, x)| describe_field(x, i == 0))
329                .for_each(|value| describe.extend(value));
330
331            if let Some(extra_fields) = &struct_attributes.extra_fields {
332                describe.extend(quote! {
333                    Into::<#extra_fields>::into(self).describe(writer, ctx.pad(Self::struct_pad()))?;
334                })
335            }
336
337            describe.extend(quote!(Ok(())));
338            describe
339        }
340    }
341}
342
343// Will generate the describe for a specific field
344fn describe_field(field: &StructField, first_field: bool) -> TokenStream {
345    let title_name = field.field_name.to_case(Case::Title);
346    let ident = &field.ident;
347
348    if field.attr.flatten {
349        quote! {
350            self.#ident.describe(writer, ctx.pad(Self::struct_pad()))?;
351        }
352    } else {
353        let title = quote! {
354            ctx.write_title(writer, #title_name, #first_field)?;
355        };
356
357        let value = if field.attr.output_table {
358            quote! {
359                ctx.describe_table(&self.#ident, writer)?;
360            }
361        } else {
362            field_getter(
363                &field,
364                quote! {
365                    describe(writer, ctx.indent(Self::struct_pad(), #title_name.len()))?;
366                },
367            )
368        };
369
370        quote! {
371            #title
372            #value
373        }
374    }
375}
376
377// A helper function that handle all the code to map/into/resolve_option
378// Need a method to call after the getter
379fn field_getter(field: &StructField, method: TokenStream) -> TokenStream {
380    let ident = &field.ident;
381
382    let value = match (&field.attr.map, &field.attr.into) {
383        (Some(func), _) => {
384            quote! {
385                #func(#ident)
386            }
387        }
388        (_, Some(into)) => {
389            quote! {
390                Into::<#into>::into(#ident)
391            }
392        }
393        (_, _) => {
394            quote! {
395                #ident
396            }
397        }
398    };
399
400    if path_is_option(&field.typ) && field.attr.resolve_option {
401        quote! {
402            if let Some(#ident) = &self.#ident {
403                #value.#method
404            } else {
405                self.#ident.#method
406            }
407        }
408    } else {
409        quote! {
410            let #ident = &self.#ident;
411            #value.#method
412        }
413    }
414}
415
416// Generate decriptor Trait impl for Enum.
417fn generate_enum_decriptor(input: ItemEnum) -> proc_macro::TokenStream {
418    let enum_name = &input.ident;
419
420    let mut match_fields = quote! {};
421
422    for variant in input.variants {
423        let name = variant.ident;
424        let field_attributes = parse::extract_field_attributes(&variant.attrs);
425
426        let value = if let Some(rename) = field_attributes.rename_description {
427            quote!(#rename)
428        } else {
429            quote!(stringify!(#name))
430        };
431
432        match_fields.extend(quote! {
433            #enum_name::#name => #value.to_string(),
434        })
435    }
436
437    let to_field = quote! {
438        match self {
439            #match_fields
440        }
441    };
442
443    let describe = quote! {
444        ctx.write_value(writer, self.to_field(""))
445    };
446    generate_trait(&input.ident, describe, to_field, None, None, None, None).into()
447}
448
449fn generate_trait(
450    name: &Ident,
451    describe: TokenStream,
452    to_field: TokenStream,
453    pad: Option<TokenStream>,
454    default_headers: Option<TokenStream>,
455    headers: Option<TokenStream>,
456    header_name: Option<TokenStream>,
457) -> TokenStream {
458    let default_headers = match &default_headers {
459        None => quote! {},
460        Some(headers) => quote! {
461            fn default_headers() -> Vec<String> {
462                #headers
463            }
464        },
465    };
466    let headers = match &headers {
467        None => quote! {},
468        Some(headers) => quote! {
469            fn headers() -> Vec<String> {
470                #headers
471            }
472        },
473    };
474
475    let header_name = match &header_name {
476        None => quote! {},
477        Some(header_name) => quote! {
478            fn header_name(header: &str) -> Option<String> {
479                #header_name
480            }
481        },
482    };
483
484    let pad = match &pad {
485        None => quote! {},
486        Some(pad) => quote! {
487            fn struct_pad() -> usize {
488                #pad
489            }
490        },
491    };
492
493    quote! {
494        impl descriptor::Describe for #name {
495            fn describe<W>(&self, writer: &mut W, ctx: descriptor::Context) -> std::io::Result<()>
496            where
497                W: std::io::Write,
498            {
499                #describe
500            }
501
502            #header_name
503            #headers
504            #default_headers
505            #pad
506
507            fn to_field(&self, field_name: &str) -> String {
508                #to_field
509            }
510        }
511    }
512}
513
514fn path_is_option(ty: &Type) -> bool {
515    match ty {
516        Type::Path(TypePath { path, .. }) => {
517            path.leading_colon.is_none()
518                && path.segments.len() == 1
519                && path.segments.iter().next().unwrap().ident == "Option"
520        }
521        _ => false,
522    }
523}