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}