structform_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::*;
4
5#[proc_macro_derive(StructForm, attributes(structform))]
6pub fn derive_structform(input: TokenStream) -> TokenStream {
7    let input = parse_macro_input!(input as DeriveInput);
8    let form_ident = input.ident.clone();
9    let field_enum_ident = field_enum_ident_transform(&form_ident);
10
11    let input_struct_data = match input.data {
12        Data::Struct(data) => data,
13        _ => panic!("StructForm can only be derived for structs"),
14    };
15    let container_attrs: FormContainerAttribute = input
16        .attrs
17        .iter()
18        .find(|attr| attr.path.is_ident("structform"))
19        .map(|attr| {
20            attr.parse_args()
21                .expect("Failed to parse the #[structform] attr on the container")
22        })
23        .expect("Require a #[structform] attribute on the container");
24    let model = container_attrs.model;
25
26    let enriched_fields = enrich_fields(&input_struct_data);
27
28    let (input_names, input_fields_type): (Vec<(Ident, Ident)>, Vec<Type>) = enriched_fields
29        .iter()
30        .filter_map(|field| match &field.ty {
31            FieldType::Input { input_type } => Some((field.names(), input_type.clone())),
32            _ => None,
33        })
34        .unzip();
35    let (input_fields_snake_case, input_fields_pascal_case): (Vec<Ident>, Vec<Ident>) =
36        input_names.into_iter().unzip();
37
38    let (option_form_names, option_form_fields_type): (Vec<(Ident, Ident)>, Vec<Type>) =
39        enriched_fields
40            .iter()
41            .filter_map(|field| match &field.ty {
42                FieldType::OptionalSubform { subform_type } => {
43                    Some((field.names(), subform_type.clone()))
44                }
45                _ => None,
46            })
47            .unzip();
48    let (option_form_fields_snake_case, option_form_fields_pascal_case): (Vec<Ident>, Vec<Ident>) =
49        option_form_names.into_iter().unzip();
50    let option_form_fields_type_field_enum: Vec<Ident> = option_form_fields_type
51        .iter()
52        .map(type_to_field_enum_ident)
53        .collect();
54
55    let option_form_fields_toggles_pascal_case: Vec<Ident> = option_form_fields_pascal_case
56        .iter()
57        .map(|field_ident| Ident::new(&format!("Toggle{}", field_ident), field_ident.span()))
58        .collect();
59
60    let (list_form_names, list_form_fields_type): (Vec<(Ident, Ident)>, Vec<Type>) =
61        enriched_fields
62            .iter()
63            .filter_map(|field| match &field.ty {
64                FieldType::ListSubform { subform_type } => {
65                    Some((field.names(), subform_type.clone()))
66                }
67                _ => None,
68            })
69            .unzip();
70    let (list_form_fields_snake_case, list_form_fields_pascal_case): (Vec<Ident>, Vec<Ident>) =
71        list_form_names.into_iter().unzip();
72    let list_form_fields_type_field_enum: Vec<Ident> = list_form_fields_type
73        .iter()
74        .map(type_to_field_enum_ident)
75        .collect();
76
77    let list_form_fields_add_pascal_case: Vec<Ident> = list_form_fields_pascal_case
78        .iter()
79        .map(|field_ident| Ident::new(&format!("Add{}", field_ident), field_ident.span()))
80        .collect();
81    let list_form_fields_remove_pascal_case: Vec<Ident> = list_form_fields_pascal_case
82        .iter()
83        .map(|field_ident| Ident::new(&format!("Remove{}", field_ident), field_ident.span()))
84        .collect();
85
86    let (subform_names, subform_fields_type): (Vec<(Ident, Ident)>, Vec<Type>) = enriched_fields
87        .iter()
88        .filter_map(|field| match &field.ty {
89            FieldType::Subform { subform_type } => Some((field.names(), subform_type.clone())),
90            _ => None,
91        })
92        .unzip();
93    let (subform_fields_snake_case, subform_fields_pascal_case): (Vec<Ident>, Vec<Ident>) =
94        subform_names.into_iter().unzip();
95    let subform_fields_type_field_enum: Vec<Ident> = subform_fields_type
96        .iter()
97        .map(type_to_field_enum_ident)
98        .collect();
99
100    let submit_attempted_fields_snake_case: Vec<Ident> = enriched_fields
101        .iter()
102        .filter_map(|field| match &field.ty {
103            FieldType::SubmitAttempted => Some(field.snake_case_ident.clone()),
104            _ => None,
105        })
106        .collect();
107
108    let field_enum = quote! {
109        #[derive(Debug)]
110        pub enum #field_enum_ident {
111            #(#input_fields_pascal_case,)*
112            #(#option_form_fields_toggles_pascal_case,)*
113            #(#option_form_fields_pascal_case(#option_form_fields_type_field_enum),)*
114            #(#list_form_fields_add_pascal_case,)*
115            #(#list_form_fields_pascal_case(usize, #list_form_fields_type_field_enum),)*
116            #(#list_form_fields_remove_pascal_case(usize),)*
117            #(#subform_fields_pascal_case(#subform_fields_type_field_enum),)*
118        }
119    };
120
121    let impl_new = if container_attrs.flatten {
122        quote! {
123            fn new(model: &#model) -> #form_ident {
124                #form_ident {
125                    #(#input_fields_snake_case: <#input_fields_type>::new(&model),)*
126                    #(#submit_attempted_fields_snake_case: false,)*
127                }
128            }
129        }
130    } else {
131        quote! {
132            fn new(model: &#model) -> #form_ident {
133                #form_ident {
134                    #(#input_fields_snake_case: <#input_fields_type>::new(&model.#input_fields_snake_case),)*
135                    #(#option_form_fields_snake_case: model.#option_form_fields_snake_case.as_ref().map(<#option_form_fields_type>::new),)*
136                    #(#list_form_fields_snake_case: model.#list_form_fields_snake_case.iter().map(<#list_form_fields_type>::new).collect(),)*
137                    #(#subform_fields_snake_case: <#subform_fields_type>::new(&model.#subform_fields_snake_case),)*
138                    #(#submit_attempted_fields_snake_case: false,)*
139                }
140            }
141        }
142    };
143
144    let impl_submit = container_attrs
145        .submit_with
146        .map(|submit_with| {
147            quote! {
148                fn submit(&mut self) -> Result<#model, structform::ParseError> {
149                    #(self.#submit_attempted_fields_snake_case = true;)*
150                    #submit_with(self)
151                }
152            }
153        })
154        .unwrap_or(if container_attrs.flatten {
155            quote! {
156                fn submit(&mut self) -> Result<#model, structform::ParseError> {
157                    #(self.#submit_attempted_fields_snake_case = true;)*
158                    #(self.#input_fields_snake_case.submit())*
159                }
160            }
161        } else {
162            quote! {
163                fn submit(&mut self) -> Result<#model, structform::ParseError> {
164                    #(self.#submit_attempted_fields_snake_case = true;)*
165                    self.submit_update(<#model>::default())
166                }
167            }
168        });
169
170    let impl_submit_update = if container_attrs.flatten {
171        quote! {
172            fn submit_update(&mut self, mut model: #model) -> Result<#model, structform::ParseError> {
173                #(self.#submit_attempted_fields_snake_case = true;)*
174                #(self.#input_fields_snake_case.submit())*
175            }
176        }
177    } else {
178        quote! {
179            fn submit_update(&mut self, mut model: #model) -> Result<#model, structform::ParseError> {
180                #(self.#submit_attempted_fields_snake_case = true;)*
181
182                #(let #input_fields_snake_case = self.#input_fields_snake_case.submit();)*
183                #(let #option_form_fields_snake_case = self.#option_form_fields_snake_case.as_mut().map(|inner_form| {
184                    model.#option_form_fields_snake_case
185                        .clone()
186                        .map(|inner_model| inner_form.submit_update(inner_model))
187                        .unwrap_or_else(|| inner_form.submit())
188                }).transpose();)*
189                #(let #list_form_fields_snake_case = self.#list_form_fields_snake_case.iter_mut().enumerate().map(|(i, inner_form)| {
190                    model.#list_form_fields_snake_case
191                        .get(i)
192                        .map(|inner_model| inner_form.submit_update(inner_model.clone()))
193                        .unwrap_or_else(|| inner_form.submit())
194                }).collect::<Result<Vec<_>,_>>();)*
195                #(let #subform_fields_snake_case = self.#subform_fields_snake_case.submit_update(model.#subform_fields_snake_case.clone());)*
196
197                #(model.#input_fields_snake_case = #input_fields_snake_case?;)*
198                #(model.#option_form_fields_snake_case = #option_form_fields_snake_case?;)*
199                #(model.#list_form_fields_snake_case = #list_form_fields_snake_case?;)*
200                #(model.#subform_fields_snake_case = #subform_fields_snake_case?;)*
201                Ok(model)
202            }
203        }
204    };
205
206    let impl_set_input = quote! {
207        fn set_input(&mut self, field: #field_enum_ident, value: String) {
208            match field {
209                #(#field_enum_ident::#input_fields_pascal_case => self.#input_fields_snake_case.set_input(value),)*
210                #(#field_enum_ident::#option_form_fields_toggles_pascal_case => {
211                    if self.#option_form_fields_snake_case.is_some() {
212                        self.#option_form_fields_snake_case = None;
213                    } else {
214                        self.#option_form_fields_snake_case = Some(#option_form_fields_type::default());
215                    }
216                },)*
217                #(#field_enum_ident::#option_form_fields_pascal_case(subfield) => {
218                    self.#option_form_fields_snake_case
219                        .as_mut()
220                        .map(|inner_form| inner_form.set_input(subfield, value));
221                },)*
222                #(#field_enum_ident::#list_form_fields_add_pascal_case => {
223                    self.#list_form_fields_snake_case
224                        .push(#list_form_fields_type::default());
225                },)*
226                #(#field_enum_ident::#list_form_fields_pascal_case(i, subfield) => {
227                    self.#list_form_fields_snake_case
228                        .get_mut(i)
229                        .map(|inner_form| inner_form.set_input(subfield, value));
230                },)*
231                #(#field_enum_ident::#list_form_fields_remove_pascal_case(i) => {
232                    if i < self.#list_form_fields_snake_case.len() {
233                        self.#list_form_fields_snake_case.remove(i);
234                    }
235                },)*
236
237                #(#field_enum_ident::#subform_fields_pascal_case(subfield) => {
238                    self.#subform_fields_snake_case.set_input(subfield, value);
239                },)*
240            }
241        }
242    };
243
244    let impl_submit_attempted = quote! {
245        fn submit_attempted(&self) -> bool {
246            false #(|| self.#submit_attempted_fields_snake_case)*
247        }
248    };
249
250    let impl_is_empty = quote! {
251        fn is_empty(&self) -> bool {
252            true
253            #(&& self.#input_fields_snake_case.is_empty())*
254            #(&& self.#option_form_fields_snake_case.as_ref().map(|inner_form| inner_form.is_empty()).unwrap_or(true))*
255            #(&& self.#list_form_fields_snake_case.iter().all(|inner_form| inner_form.is_empty()))*
256            #(&& self.#subform_fields_snake_case.is_empty())*
257        }
258    };
259
260    let impl_form = quote! {
261        impl structform::StructForm<#model> for #form_ident {
262            type Field = #field_enum_ident;
263
264            #impl_new
265            #impl_submit
266            #impl_submit_update
267            #impl_set_input
268            #impl_submit_attempted
269            #impl_is_empty
270        }
271    };
272
273    (quote! {
274        #field_enum
275
276        #impl_form
277    })
278    .into()
279}
280
281fn snake_to_pascal_case(snake: &str) -> String {
282    snake
283        .split('_')
284        .map(|s| {
285            let (head, tail) = s.split_at(1);
286            format!("{}{}", head.to_uppercase(), tail)
287        })
288        .collect::<Vec<_>>()
289        .join("")
290}
291
292fn is_option(field: &Field) -> bool {
293    if let Type::Path(TypePath { path, .. }) = &field.ty {
294        let path_ident = &path.segments.first().unwrap().ident;
295        path_ident == &Ident::new("Option", path_ident.span())
296    } else {
297        false
298    }
299}
300
301fn is_vec(field: &Field) -> bool {
302    if let Type::Path(TypePath { path, .. }) = &field.ty {
303        let path_ident = &path.segments.first().unwrap().ident;
304        path_ident == &Ident::new("Vec", path_ident.span())
305    } else {
306        false
307    }
308}
309
310fn parse_option_type_generic_type(option_type: &Type) -> Type {
311    match option_type {
312        Type::Path(TypePath { path, .. }) => match &path.segments.first().unwrap().arguments {
313            PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) => {
314                match args.first().unwrap() {
315                    GenericArgument::Type(generic_type) => generic_type.clone(),
316                    _ => panic!("Option's type argument was not a generic type"),
317                }
318            }
319            _ => panic!("Option type did not have an angle bracketed generic argument"),
320        },
321        _ => panic!("Option type did not have a generic argument"),
322    }
323}
324
325fn parse_vec_type_generic_type(vec_type: &Type) -> Type {
326    match vec_type {
327        Type::Path(TypePath { path, .. }) => match &path.segments.first().unwrap().arguments {
328            PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) => {
329                match args.first().unwrap() {
330                    GenericArgument::Type(generic_type) => generic_type.clone(),
331                    _ => panic!("Vec's type argument was not a generic type"),
332                }
333            }
334            _ => panic!("Vec type did not have an angle bracketed generic argument"),
335        },
336        _ => panic!("Vec type did not have a generic argument"),
337    }
338}
339
340fn type_to_field_enum_ident(ty: &Type) -> Ident {
341    match ty {
342        Type::Path(TypePath { path, .. }) => {
343            field_enum_ident_transform(&path.segments.first().unwrap().ident)
344        }
345        _ => panic!("Option's generic type was not a TypePath"),
346    }
347}
348
349fn field_enum_ident_transform(ident: &Ident) -> Ident {
350    Ident::new(&format!("{}Field", ident), ident.span())
351}
352
353struct FormContainerAttribute {
354    model: Ident,
355    submit_with: Option<Ident>,
356    flatten: bool,
357}
358
359impl parse::Parse for FormContainerAttribute {
360    fn parse(parse_buffer: &syn::parse::ParseBuffer<'_>) -> parse::Result<Self> {
361        let meta_list = parse_buffer.parse_terminated::<_, syn::token::Comma>(NestedMeta::parse)?;
362        let model: String = meta_list
363            .iter()
364            .filter_map(|arg| match arg {
365                NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
366                    if path.is_ident("model") =>
367                {
368                    match lit {
369                        Lit::Str(lit) => Some(lit.value()),
370                        _ => None,
371                    }
372                }
373                _ => None,
374            })
375            .next()
376            .expect(
377                "Expected to find an attribute indicating the model type: #[structform(model = \"???\")]",
378            );
379        let model = Ident::new(&model, parse_buffer.span());
380        let submit_with: Option<String> = meta_list
381            .iter()
382            .filter_map(|arg| match arg {
383                NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
384                    if path.is_ident("submit_with") =>
385                {
386                    match lit {
387                        Lit::Str(lit) => Some(lit.value()),
388                        _ => None,
389                    }
390                }
391                _ => None,
392            })
393            .next();
394        let submit_with =
395            submit_with.map(|submit_with| Ident::new(&submit_with, parse_buffer.span()));
396        let flatten = meta_list.iter().any(
397            |arg| matches!(arg, NestedMeta::Meta(Meta::Path(path)) if path.is_ident("flatten")),
398        );
399
400        Ok(FormContainerAttribute {
401            model,
402            submit_with,
403            flatten,
404        })
405    }
406}
407
408#[derive(Default)]
409struct FormFieldAttribute {
410    submit_attempted: bool,
411    subform: bool,
412}
413
414impl parse::Parse for FormFieldAttribute {
415    fn parse(parse_buffer: &syn::parse::ParseBuffer<'_>) -> parse::Result<Self> {
416        let meta_list = parse_buffer.parse_terminated::<_, syn::token::Comma>(NestedMeta::parse)?;
417        let submit_attempted = meta_list.iter().any(|arg| matches!(arg, NestedMeta::Meta(Meta::Path(path)) if path.is_ident("submit_attempted")));
418        let subform = meta_list.iter().any(
419            |arg| matches!(arg, NestedMeta::Meta(Meta::Path(path)) if path.is_ident("subform")),
420        );
421
422        Ok(FormFieldAttribute {
423            submit_attempted,
424            subform,
425        })
426    }
427}
428
429struct RichField {
430    snake_case_ident: Ident,
431    pascal_case_ident: Ident,
432    ty: FieldType,
433}
434
435impl RichField {
436    fn names(&self) -> (Ident, Ident) {
437        (
438            self.snake_case_ident.clone(),
439            self.pascal_case_ident.clone(),
440        )
441    }
442}
443
444fn enrich_fields(struct_data: &DataStruct) -> Vec<RichField> {
445    struct_data
446        .fields
447        .iter()
448        .map(|field| {
449            let snake_case_ident = field
450                .ident
451                .clone()
452                .expect("Only normal structs are supported.");
453            let pascal_case_ident = Ident::new(
454                &snake_to_pascal_case(&snake_case_ident.to_string()),
455                snake_case_ident.span(),
456            );
457            let attrs = field
458                .attrs
459                .iter()
460                .filter(|attr| attr.path.is_ident("structform"))
461                .map(|attr| {
462                    attr.parse_args::<FormFieldAttribute>()
463                        .expect("failed to parse attrs on a field")
464                })
465                .next()
466                .unwrap_or_default();
467
468            let ty = if attrs.submit_attempted {
469                FieldType::SubmitAttempted
470            } else if attrs.subform {
471                FieldType::Subform {
472                    subform_type: field.ty.clone(),
473                }
474            } else if is_option(field) {
475                FieldType::OptionalSubform {
476                    subform_type: parse_option_type_generic_type(&field.ty),
477                }
478            } else if is_vec(field) {
479                FieldType::ListSubform {
480                    subform_type: parse_vec_type_generic_type(&field.ty),
481                }
482            } else {
483                FieldType::Input {
484                    input_type: field.ty.clone(),
485                }
486            };
487
488            RichField {
489                snake_case_ident,
490                pascal_case_ident,
491                ty,
492            }
493        })
494        .collect()
495}
496
497enum FieldType {
498    Input { input_type: Type },
499    Subform { subform_type: Type },
500    OptionalSubform { subform_type: Type },
501    ListSubform { subform_type: Type },
502    SubmitAttempted,
503}