formy_derive/
lib.rs

1//! The derive macro implementation for `formy`.
2
3extern crate proc_macro;
4
5use inflector::Inflector;
6use proc_macro::TokenStream;
7use quote::{quote, quote_spanned};
8use syn::spanned::Spanned;
9use syn::{self, Fields};
10
11/// The macro that automatically implements the Form trait.
12#[proc_macro_derive(Form, attributes(input, label))]
13pub fn formy_derive(input: TokenStream) -> TokenStream {
14    let ast = syn::parse(input).unwrap();
15    impl_formy_derive(&ast)
16}
17
18// https://doc.rust-lang.org/book/ch19-06-macros.html
19
20struct InputAttribute {
21    pub name: &'static str,
22    pub any_value: bool,
23    pub accepted_values: &'static[&'static str],
24}
25
26fn get_all_possible_attributes() -> Vec<InputAttribute> {
27    let mut x = vec![];
28
29    // Add all: https://www.w3schools.com/tags/tag_input.asp
30
31    x.push(InputAttribute {
32        name: "type",
33        any_value: false,
34        accepted_values: &[
35            "text",
36            "password",
37            "email",
38            "checkbox",
39            "color",
40            "date",
41            "datetime-local",
42            "file",
43            "hidden",
44            "image",
45            "month",
46            "number",
47            "radio",
48            "range",
49            "reset",
50            "search",
51            "submit",
52            "tel",
53            "time",
54            "url",
55            "week",
56        ],
57    });
58    x.push(InputAttribute {
59        name: "alt",
60        any_value: true,
61        accepted_values: &[],
62    });
63    x.push(InputAttribute {
64        name: "name",
65        any_value: true,
66        accepted_values: &[],
67    });
68    x.push(InputAttribute {
69        name: "id",
70        any_value: true,
71        accepted_values: &[],
72    });
73    x.push(InputAttribute {
74        name: "class",
75        any_value: true,
76        accepted_values: &[],
77    });
78    x.push(InputAttribute {
79        name: "autocomplete",
80        any_value: false,
81        accepted_values: &["on", "off"],
82    });
83    x.push(InputAttribute {
84        name: "autofocus",
85        any_value: false,
86        accepted_values: &["autofocus"],
87    });
88    x.push(InputAttribute {
89        name: "checked",
90        any_value: false,
91        accepted_values: &["checked"],
92    });
93    x.push(InputAttribute {
94        name: "dirname",
95        any_value: true,
96        accepted_values: &[],
97    });
98    x.push(InputAttribute {
99        name: "disabled",
100        any_value: false,
101        accepted_values: &["disabled"],
102    });
103    x.push(InputAttribute {
104        name: "form",
105        any_value: true,
106        accepted_values: &[],
107    });
108    x.push(InputAttribute {
109        name: "formaction",
110        any_value: true,
111        accepted_values: &[],
112    });
113    x.push(InputAttribute {
114        name: "formenctype",
115        any_value: false,
116        accepted_values: &["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"],
117    });
118    x.push(InputAttribute {
119        name: "formmethod",
120        any_value: false,
121        accepted_values: &["post", "get"],
122    });
123    x.push(InputAttribute {
124        name: "formnovalidate",
125        any_value: false,
126        accepted_values: &["formnovalidate"],
127    });
128    x.push(InputAttribute {
129        name: "formtarget",
130        any_value: false,
131        accepted_values: &["_blank", "_self", "_parent", "_top"],
132    });
133    x.push(InputAttribute {
134        name: "height",
135        any_value: true,
136        accepted_values: &[],
137    });
138    x.push(InputAttribute {
139        name: "width",
140        any_value: true,
141        accepted_values: &[],
142    });
143    x.push(InputAttribute {
144        name: "list",
145        any_value: true,
146        accepted_values: &[],
147    });
148    x.push(InputAttribute {
149        name: "max",
150        any_value: true,
151        accepted_values: &[],
152    });
153    x.push(InputAttribute {
154        name: "min",
155        any_value: true,
156        accepted_values: &[],
157    });
158    x.push(InputAttribute {
159        name: "minlenght",
160        any_value: true,
161        accepted_values: &[],
162    });
163    x.push(InputAttribute {
164        name: "multiple",
165        any_value: false,
166        accepted_values: &["multiple"],
167    });
168    x.push(InputAttribute {
169        name: "pattern",
170        any_value: true,
171        accepted_values: &[],
172    });
173    x.push(InputAttribute {
174        name: "placeholder",
175        any_value: true,
176        accepted_values: &[],
177    });
178    x.push(InputAttribute {
179        name: "readonly",
180        any_value: false,
181        accepted_values: &["readonly"],
182    });
183    x.push(InputAttribute {
184        name: "required",
185        any_value: false,
186        accepted_values: &["required"],
187    });
188    x.push(InputAttribute {
189        name: "size",
190        any_value: true,
191        accepted_values: &[],
192    });
193    x.push(InputAttribute {
194        name: "src",
195        any_value: true,
196        accepted_values: &[],
197    });
198    x.push(InputAttribute {
199        name: "step",
200        any_value: true,
201        accepted_values: &[],
202    });
203    x.push(InputAttribute {
204        name: "value",
205        any_value: true,
206        accepted_values: &[],
207    });
208
209    x
210}
211
212fn get_meta_list(nested_meta: &syn::MetaList) -> Result<Vec<(&syn::Path, &syn::Lit)>, TokenStream> {
213    let mut list = vec![];
214    for v in &nested_meta.nested {
215        match v {
216            syn::NestedMeta::Meta(m) => {
217                if let syn::Meta::NameValue(value) = &m {
218                    list.push((&value.path, &value.lit));
219                } else {
220                    return Err(
221                        quote_spanned! {m.span()=> compile_error!("Must be a named value.")}.into(),
222                    );
223                }
224            }
225            x => {
226                return Err(
227                    quote_spanned! {x.span()=> compile_error!("Invalid meta type.")}.into(),
228                );
229            }
230        }
231    }
232
233    Ok(list)
234}
235
236fn impl_formy_derive(ast: &syn::DeriveInput) -> TokenStream {
237    let name = &ast.ident;
238
239    let input_types = get_all_possible_attributes();
240
241    if let syn::Data::Struct(syn::DataStruct {
242        struct_token,
243        fields,
244        semi_token: _,
245    }) = &ast.data
246    {
247        let mut inputs = Vec::new();
248
249        match fields {
250            Fields::Named(named) => {
251                for field in named.named.iter() {
252                    // Defaults
253                    let mut input_attributes = Vec::new();
254
255                    let mut name_added = false;
256                    let mut id_added = false;
257                    let mut input_name = field.ident.clone().unwrap().to_string();
258                    let mut input_id = field.ident.clone().unwrap().to_string();
259                    let mut label = None;
260
261                    // Parse macro attributes.
262                    for attr in field.attrs.iter() {
263                        // Everything is under the meta list attribute "input",
264                        // like #[input(name = "myfield")]
265                        //
266                        if attr.path.is_ident("input") {
267                            let meta = attr.parse_meta().unwrap();
268
269                            if let syn::Meta::List(nested_meta) = meta {
270                                match get_meta_list(&nested_meta) {
271                                    Ok(ref values) => {
272                                        for value in values {
273                                            match value.1 {
274                                                syn::Lit::Str(valstr) => {
275                                                    let val = valstr.value();
276
277                                                    let mut found = false;
278
279                                                    for inp_atr in &input_types {
280                                                        if value.0.is_ident(inp_atr.name) {
281                                                            found = true;
282
283                                                            if value.0.is_ident("name") {
284                                                                name_added = true;
285                                                                input_name = val.clone();
286                                                            }
287                                                            else if value.0.is_ident("id") {
288                                                                id_added = true;
289                                                                input_id = val.clone();
290                                                            }
291
292                                                            if inp_atr.any_value {
293                                                                input_attributes.push(format!("{}=\"{}\"", inp_atr.name, val));
294                                                            }
295                                                            else if inp_atr.accepted_values.iter().any(|x| x.eq(&val)) {
296                                                                input_attributes.push(format!("{}=\"{}\"", inp_atr.name, val));
297                                                            }
298                                                            else {
299                                                                let error_msg = 
300                                                                    format!("'input' macro attribute value for '{}' must be one of the following: {:?}.", 
301                                                                    inp_atr.name,
302                                                                    inp_atr.accepted_values
303                                                                    );
304                                                                return quote_spanned!{valstr.span()=> compile_error!(#error_msg);}.into();
305                                                            }
306                                                            break;
307                                                        }
308                                                    }
309                                                    if !found {
310                                                        return quote_spanned!{value.0.span()=> compile_error!("Unrecognized value name.");}.into();
311                                                    }
312                                                }
313                                                lit => {
314                                                    return quote_spanned! { lit.span()=> compile_error!("Invalid data type.");}.into();
315                                                }
316                                            }
317                                        }
318                                    }
319                                    Err(e) => return e,
320                                }
321                            } else {
322                                return quote_spanned! { attr.path.span()=> compile_error!("'input' macro attribute must be a list, e.g: #[input(name = \"x\", type=\"text\")].");}.into();
323                            }
324                        }
325                        else if attr.path.is_ident("label") {
326                            let meta = attr.parse_meta().unwrap();
327
328                            if let syn::Meta::NameValue(name_val) = &meta {
329                                if let syn::Lit::Str(val) = &name_val.lit {
330                                    label = Some(val.value());
331                                }
332                                else {
333                                    return quote_spanned! {name_val.lit.span()=> compile_error!("Value must be a str.");}.into();
334                                }
335                            }
336                            else {
337                                return quote_spanned! {meta.span()=> compile_error!("Must be a name value e.g: label = \"Username:\"");}.into();
338                            }
339
340                        }
341                        else {
342                            return quote_spanned! {attr.path.span()=> compile_error!("Unrecognized attribute name.");}.into();
343                        }
344                    }
345
346                    if !name_added {
347                        input_attributes.push(format!("name=\"{}\"", input_name));
348                    }
349
350                    if !id_added {
351                        input_attributes.push(format!("id=\"{}\"", input_name));
352                    }
353
354                    let mut inp = String::from("\t<input");
355
356                    for attr in &input_attributes {
357                        inp.push_str(" ");
358                        inp.push_str(attr);
359                    }
360
361                    inp.push_str(">\n");
362
363                    if let Some(label) = label {
364                        let label = format!("\t<label for =\"{}\">{}</label>\n", input_id, label);
365                        inputs.push(label);
366                    }
367                    else {
368                        let label = input_name.to_title_case();
369                        let label = format!("\t<label for =\"{}\">{}:</label>\n", input_id, label);
370                        inputs.push(label);
371                    }
372                    inputs.push(inp);
373                }
374            }
375            _ => {
376                return quote_spanned! { struct_token.span=> std::compile_error!("This macro only accepts named fields."); }.into();
377            }
378        }
379
380        let gen = quote! {
381            impl formy::Form for #name {
382                fn to_form() -> String {
383                    let mut html = String::new();
384                    html.push_str("<form>\n");
385                    #( html.push_str(#inputs); )*
386                    html.push_str("</form>");
387
388                    html
389                }
390            }
391        };
392        gen.into()
393    } else {
394        return quote_spanned! {ast.span()=> compile_error!("Only structs are supported.");}.into();
395    }
396}