roadblk_expand/
lib.rs

1use proc_macro2::TokenStream;
2use procmeta::prelude::*;
3use quote::quote;
4use syn::LitStr;
5use syn::{Data, DataEnum, DataStruct, DeriveInput, Field, Fields, LitInt};
6
7#[derive(MetaParser)]
8pub enum ValidateAttr {
9    #[name("validate")]
10    DefaultValidate,
11
12    #[name("validate")]
13    DefaultValidateWithMsg(LitStr),
14
15    #[name("validate")]
16    Validate(ConstraintTypeAttr),
17
18    #[name("validate")]
19    ValidateWithMsg(ConstraintTypeAttr, LitStr),
20}
21
22#[derive(MetaParser)]
23pub enum NameAttr {
24    Name(LitStr),
25}
26
27#[derive(MetaParser)]
28pub enum ConstraintTypeAttr {
29    Length(LitInt, LitInt),
30
31    Range(Lit, Lit),
32
33    #[name("validator")]
34    Validator,
35
36    #[name("validator")]
37    SpecialValidator(Type),
38
39    #[name("reg")]
40    Regex(LitStr),
41
42    Enumer(Type),
43}
44
45pub struct ParsedFiled<'a> {
46    pub field_name: Option<String>,
47    pub field_desc: Option<String>,
48    pub ident: TokenStream,
49    pub field: &'a Field,
50    pub constraints: Vec<(ConstraintTypeAttr, Option<String>)>,
51}
52
53impl<'a> ParsedFiled<'a> {
54    fn try_from(
55        field_name: Option<String>,
56        ident: TokenStream,
57        field: &'a Field,
58    ) -> syn::Result<Self> {
59        let mut constraints = vec![];
60        let mut field_desc = None;
61        for attr in &field.attrs {
62            let parsed_attr = ValidateAttr::try_from(attr);
63            if let Ok(parsed_attr) = parsed_attr {
64                let constraint = match parsed_attr {
65                    ValidateAttr::Validate(constraint) => (constraint, None),
66                    ValidateAttr::ValidateWithMsg(constraint, msg) => {
67                        (constraint, Some(msg.value()))
68                    }
69                    ValidateAttr::DefaultValidate => (ConstraintTypeAttr::Validator, None),
70                    ValidateAttr::DefaultValidateWithMsg(msg) => {
71                        (ConstraintTypeAttr::Validator, Some(msg.value()))
72                    }
73                };
74                constraints.push(constraint);
75                continue;
76            }
77            let parsed_attr = NameAttr::try_from(attr);
78            if let Ok(parsed_attr) = parsed_attr {
79                if field_desc.is_some() {
80                    return Err(Error::new(Span::call_site(), "desc attr duplicated"));
81                }
82                let NameAttr::Name(desc) = parsed_attr;
83                field_desc = Some(desc.value());
84            }
85        }
86        Ok(Self {
87            ident,
88            field,
89            field_name,
90            field_desc,
91            constraints,
92        })
93    }
94
95    pub fn validate_quote(self) -> Result<TokenStream> {
96        let mut result_token = quote!();
97        let field_ident = self.ident;
98        let field_name = self.field_name.get_token_stream();
99        let field_desc = self.field_desc.get_token_stream();
100        let mut field_ty = self.field.ty.clone();
101        type_add_colon2(&mut field_ty);
102        for constraint in self.constraints {
103            let item_msg = constraint.1;
104            let mut validate_token = match constraint.0 {
105                ConstraintTypeAttr::Length(min, max) => {
106                    quote! {
107                        let validated = <LengthValidator::<#min, #max> as Validator<#field_ty>>::validate(#field_ident);
108                    }
109                }
110                ConstraintTypeAttr::Range(min, max) => {
111                    quote! {
112                        let validated = <#field_ty as RangeValidator>::validate(#field_ident, #min, #max);
113                    }
114                }
115                ConstraintTypeAttr::Validator => {
116                    quote! {
117                        let validated = <#field_ty as SelfValidator> ::validate(#field_ident);
118                    }
119                }
120                ConstraintTypeAttr::SpecialValidator(mut ty) => {
121                    type_add_colon2(&mut ty);
122                    quote! {
123                        let validated = <#ty as Validator<#field_ty>>::validate(#field_ident);
124                    }
125                }
126                ConstraintTypeAttr::Regex(regex) => {
127                    quote! {
128                        let REGEX_EXPRESS = regex!(#regex);
129                        let validated = <#field_ty as RegexValidator<#field_ty>>::validate(REGEX_EXPRESS, #field_ident);
130                    }
131                }
132                ConstraintTypeAttr::Enumer(mut ty) => {
133                    type_add_colon2(&mut ty);
134                    quote! {
135                        let validated = <#field_ty as EnumValidator<'_, #ty>>::validate(#field_ident);
136                    }
137                }
138            };
139            validate_token = match item_msg {
140                Some(msg) => quote! {
141                    #validate_token
142                    if let Err(e) = validated {
143                        return Err(ModelValidatorError {
144                            error_ty: Box::new(e.into()),
145                            error_message: Some(format!(#msg)),
146                            field_ident: #field_name,
147                            field_name: #field_desc,
148                        });
149                    }
150                },
151                None => quote! {
152                    #validate_token
153                    if let Err(e) = validated {
154                        return Err(ModelValidatorError {
155                            error_ty: Box::new(e.into()),
156                            error_message: None,
157                            field_ident: #field_name,
158                            field_name: #field_desc,
159                        });
160                    }
161                },
162            };
163            result_token = quote! {
164                #result_token
165                #validate_token
166            };
167        }
168        Ok(result_token)
169    }
170}
171
172pub fn impl_struct(data: &DataStruct) -> Result<TokenStream> {
173    let mut result_token = quote!();
174    for (index, field) in data.fields.iter().enumerate() {
175        let field_name: String;
176        let ident: TokenStream;
177        match &field.ident {
178            Some(inner) => {
179                field_name = inner.to_string();
180                ident = quote!(&self. #inner);
181            }
182            None => {
183                let index = LitInt::new(&index.to_string(), Span::call_site());
184                ident = quote!(&self. #index);
185                field_name = format!("{index}");
186            }
187        };
188        let parsed_field = ParsedFiled::try_from(Some(field_name), ident, field)?;
189        let field_validate_token = parsed_field.validate_quote()?;
190        result_token = quote! {
191            #result_token
192            #field_validate_token
193        };
194    }
195    Ok(result_token)
196}
197
198pub fn impl_enum(data: &DataEnum) -> Result<TokenStream> {
199    let mut result_token = quote!();
200    for variant in &data.variants {
201        let mut variant_token = quote!();
202        let variant_ident = &variant.ident;
203        let mut pat = quote! {};
204        let is_named_field = !matches!(&variant.fields, Fields::Unnamed(_));
205        for (index, field) in variant.fields.iter().enumerate() {
206            let ident: TokenStream;
207            let field_name: String;
208            match &field.ident {
209                Some(inner) => {
210                    ident = quote!(#inner);
211                    field_name = format!("{}.{}", variant_ident, ident);
212                }
213                None => {
214                    let unamed_ident = get_unname_field_ident(index);
215                    ident = quote!(#unamed_ident);
216                    field_name = format!("{}.{}", variant_ident, index);
217                }
218            }
219            pat = quote! {
220                #pat #ident,
221            };
222            let parsed_field = ParsedFiled::try_from(Some(field_name), ident, field)?;
223            let field_validate_token = parsed_field.validate_quote()?;
224            variant_token = quote! {
225                #variant_token
226                #field_validate_token
227            };
228        }
229        if is_named_field {
230            pat = quote! {{#pat}}
231        } else {
232            pat = quote! {(#pat)}
233        }
234        variant_token = quote! {
235            Self::#variant_ident #pat => {
236                #variant_token
237            }
238        };
239        result_token = quote! {
240            #result_token
241            #variant_token
242        }
243    }
244
245    result_token = quote! {
246        match self {
247            #result_token
248        }
249    };
250    Ok(result_token)
251}
252
253pub fn expand(input: DeriveInput) -> Result<TokenStream> {
254    let ty = &input.ident;
255    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
256    let body_token = match &input.data {
257        Data::Struct(data) => impl_struct(data)?,
258        Data::Enum(data) => impl_enum(data)?,
259        Data::Union(_) => unimplemented!(),
260    };
261    let result = quote! {
262        impl #impl_generics SelfValidator for #ty #ty_generics #where_clause {
263            #[allow(clippy::useless_format)]
264            fn validate(&self) -> Result<(), ModelValidatorError> {
265                #body_token
266                Ok(())
267            }
268        }
269
270    };
271    Ok(result)
272}