1use proc_macro::TokenStream;
14use proc_macro2::TokenStream as TokenStream2;
15use quote::{ToTokens, quote};
16use syn::{Attribute, Data, DeriveInput, Fields, Type, parse_macro_input};
17
18mod rules;
19
20fn extract_error_type(attrs: &[Attribute]) -> TokenStream2 {
21 for attr in attrs.iter().filter(|a| a.path().is_ident("validate_error")) {
22 let mut ty = None;
23 attr.parse_nested_meta(|meta| {
24 ty = Some(meta.path.to_token_stream());
25 Ok(())
26 })
27 .unwrap();
28 if let Some(t) = ty {
29 return t;
30 }
31 }
32 quote! { String }
33}
34
35fn extract_rules(attrs: &[Attribute]) -> Vec<String> {
36 let mut out = vec![];
37 for attr in attrs.iter().filter(|a| a.path().is_ident("validate")) {
38 attr.parse_nested_meta(|meta| {
39 if let Some(id) = meta.path.get_ident() {
40 out.push(id.to_string());
41 }
42 Ok(())
43 })
44 .unwrap();
45 }
46 out
47}
48
49fn is_string_type(ty: &Type) -> bool {
51 match ty {
52 Type::Path(p) => p
53 .path
54 .segments
55 .last()
56 .map(|s| s.ident == "String")
57 .unwrap_or(false),
58 Type::Reference(r) => {
59 if let Type::Path(p) = &*r.elem {
60 p.path
61 .segments
62 .last()
63 .map(|s| s.ident == "String")
64 .unwrap_or(false)
65 } else {
66 false
67 }
68 }
69 _ => false,
70 }
71}
72
73#[proc_macro_derive(Validate, attributes(validate, validate_error))]
78pub fn derive_validate(input: TokenStream) -> TokenStream {
79 let ast = parse_macro_input!(input as DeriveInput);
80 let struct_name = ast.ident;
81 let error_type = extract_error_type(&ast.attrs);
82 let struct_rules = extract_rules(&ast.attrs);
83
84 let mut ctor_params = vec![];
85 let mut ctor_assigns = vec![];
86 let mut validations = vec![];
87
88 let fields = match ast.data {
89 Data::Struct(s) => match s.fields {
90 Fields::Named(n) => n.named,
91 _ => return quote! { compile_error!("Validate supports named structs only"); }.into(),
92 },
93 _ => return quote! { compile_error!("Validate can only be used on structs"); }.into(),
94 };
95
96 for field in fields {
97 let ident = field.ident.unwrap();
98 let ty = field.ty;
99
100 ctor_params.push(quote! { #ident: #ty });
101 ctor_assigns.push(quote! { #ident });
102
103 let mut field_rules = extract_rules(&field.attrs);
104 if field_rules.iter().any(|r| r == "skip") {
105 continue;
106 }
107 if field_rules.is_empty() {
108 field_rules = struct_rules.clone();
109 }
110 if field_rules.is_empty() {
111 continue;
112 }
113
114 if !is_string_type(&ty) {
115 let msg = format!(
116 "Validation rules can only be applied to String fields: {}",
117 ident
118 );
119 return quote! { compile_error!(#msg); }.into();
120 }
121
122 for rule in field_rules {
123 let ts = match rules::dispatch(&rule, &ident) {
124 Some(ts) => ts,
125 None => {
126 let msg = format!("Unknown rule `{}`", rule);
127 return quote! { compile_error!(#msg); }.into();
128 }
129 };
130 validations.push(ts);
131 }
132 }
133
134 let out = quote! {
135 impl #struct_name {
136 #[allow(clippy::too_many_arguments)]
137 pub fn new(
138 #(#ctor_params),*
139 ) -> Result<Self, #error_type> {
140
141 type E = #error_type;
142
143 #(
144 #validations
145 )*
146
147 Ok(Self {
148 #(#ctor_assigns),*
149 })
150 }
151 }
152 };
153
154 out.into()
155}