1extern 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#[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
18struct 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 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 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 for attr in field.attrs.iter() {
263 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}