use proc_macro2::TokenStream;
use procmeta::prelude::*;
use quote::quote;
use syn::LitStr;
use syn::{Data, DataEnum, DataStruct, DeriveInput, Field, Fields, LitInt};
#[derive(MetaParser)]
pub enum ValidateAttr {
#[name("validate")]
DefaultValidate,
#[name("validate")]
DefaultValidateWithMsg(LitStr),
#[name("validate")]
Validate(ConstraintTypeAttr),
#[name("validate")]
ValidateWithMsg(ConstraintTypeAttr, LitStr),
}
#[derive(MetaParser)]
pub enum NameAttr {
Name(LitStr),
}
#[derive(MetaParser)]
pub enum ConstraintTypeAttr {
Length(LitInt, LitInt),
Range(Lit, Lit),
#[name("validator")]
Validator,
#[name("validator")]
SpecialValidator(Type),
#[name("reg")]
Regex(LitStr),
Enumer(Type),
}
pub struct ParsedFiled<'a> {
pub field_name: Option<String>,
pub field_desc: Option<String>,
pub ident: TokenStream,
pub field: &'a Field,
pub constraints: Vec<(ConstraintTypeAttr, Option<String>)>,
}
impl<'a> ParsedFiled<'a> {
fn try_from(
field_name: Option<String>,
ident: TokenStream,
field: &'a Field,
) -> syn::Result<Self> {
let mut constraints = vec![];
let mut field_desc = None;
for attr in &field.attrs {
let parsed_attr = ValidateAttr::try_from(attr);
if let Ok(parsed_attr) = parsed_attr {
let constraint = match parsed_attr {
ValidateAttr::Validate(constraint) => (constraint, None),
ValidateAttr::ValidateWithMsg(constraint, msg) => {
(constraint, Some(msg.value()))
}
ValidateAttr::DefaultValidate => (ConstraintTypeAttr::Validator, None),
ValidateAttr::DefaultValidateWithMsg(msg) => {
(ConstraintTypeAttr::Validator, Some(msg.value()))
}
};
constraints.push(constraint);
continue;
}
let parsed_attr = NameAttr::try_from(attr);
if let Ok(parsed_attr) = parsed_attr {
if field_desc.is_some() {
return Err(Error::new(Span::call_site(), "desc attr duplicated"));
}
let NameAttr::Name(desc) = parsed_attr;
field_desc = Some(desc.value());
}
}
Ok(Self {
ident,
field,
field_name,
field_desc,
constraints,
})
}
pub fn validate_quote(self) -> Result<TokenStream> {
let mut result_token = quote!();
let field_ident = self.ident;
let field_name = self.field_name.get_token_stream();
let field_desc = self.field_desc.get_token_stream();
let mut field_ty = self.field.ty.clone();
type_add_colon2(&mut field_ty);
for constraint in self.constraints {
let item_msg = constraint.1;
let mut validate_token = match constraint.0 {
ConstraintTypeAttr::Length(min, max) => {
quote! {
let validated = <LengthValidator::<#min, #max> as Validator<#field_ty>>::validate(#field_ident);
}
}
ConstraintTypeAttr::Range(min, max) => {
quote! {
let validated = <#field_ty as RangeValidator>::validate(#field_ident, #min, #max);
}
}
ConstraintTypeAttr::Validator => {
quote! {
let validated = <#field_ty as SelfValidator> ::validate(#field_ident);
}
}
ConstraintTypeAttr::SpecialValidator(mut ty) => {
type_add_colon2(&mut ty);
quote! {
let validated = <#ty as Validator<#field_ty>>::validate(#field_ident);
}
}
ConstraintTypeAttr::Regex(regex) => {
quote! {
let REGEX_EXPRESS = regex!(#regex);
let validated = <#field_ty as RegexValidator<#field_ty>>::validate(REGEX_EXPRESS, #field_ident);
}
}
ConstraintTypeAttr::Enumer(mut ty) => {
type_add_colon2(&mut ty);
quote! {
let validated = <#field_ty as EnumValidator<'_, #ty>>::validate(#field_ident);
}
}
};
validate_token = match item_msg {
Some(msg) => quote! {
#validate_token
if let Err(e) = validated {
return Err(ModelValidatorError {
error_ty: Box::new(e.into()),
error_message: Some(format!(#msg)),
field_ident: #field_name,
field_name: #field_desc,
});
}
},
None => quote! {
#validate_token
if let Err(e) = validated {
return Err(ModelValidatorError {
error_ty: Box::new(e.into()),
error_message: None,
field_ident: #field_name,
field_name: #field_desc,
});
}
},
};
result_token = quote! {
#result_token
#validate_token
};
}
Ok(result_token)
}
}
pub fn impl_struct(data: &DataStruct) -> Result<TokenStream> {
let mut result_token = quote!();
for (index, field) in data.fields.iter().enumerate() {
let field_name: String;
let ident: TokenStream;
match &field.ident {
Some(inner) => {
field_name = inner.to_string();
ident = quote!(&self. #inner);
}
None => {
let index = LitInt::new(&index.to_string(), Span::call_site());
ident = quote!(&self. #index);
field_name = format!("{index}");
}
};
let parsed_field = ParsedFiled::try_from(Some(field_name), ident, field)?;
let field_validate_token = parsed_field.validate_quote()?;
result_token = quote! {
#result_token
#field_validate_token
};
}
Ok(result_token)
}
pub fn impl_enum(data: &DataEnum) -> Result<TokenStream> {
let mut result_token = quote!();
for variant in &data.variants {
let mut variant_token = quote!();
let variant_ident = &variant.ident;
let mut pat = quote! {};
let is_named_field = !matches!(&variant.fields, Fields::Unnamed(_));
for (index, field) in variant.fields.iter().enumerate() {
let ident: TokenStream;
let field_name: String;
match &field.ident {
Some(inner) => {
ident = quote!(#inner);
field_name = format!("{}.{}", variant_ident, ident);
}
None => {
let unamed_ident = get_unname_field_ident(index);
ident = quote!(#unamed_ident);
field_name = format!("{}.{}", variant_ident, index);
}
}
pat = quote! {
#pat #ident,
};
let parsed_field = ParsedFiled::try_from(Some(field_name), ident, field)?;
let field_validate_token = parsed_field.validate_quote()?;
variant_token = quote! {
#variant_token
#field_validate_token
};
}
if is_named_field {
pat = quote! {{#pat}}
} else {
pat = quote! {(#pat)}
}
variant_token = quote! {
Self::#variant_ident #pat => {
#variant_token
}
};
result_token = quote! {
#result_token
#variant_token
}
}
result_token = quote! {
match self {
#result_token
}
};
Ok(result_token)
}
pub fn expand(input: DeriveInput) -> Result<TokenStream> {
let ty = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let body_token = match &input.data {
Data::Struct(data) => impl_struct(data)?,
Data::Enum(data) => impl_enum(data)?,
Data::Union(_) => unimplemented!(),
};
let result = quote! {
impl #impl_generics SelfValidator for #ty #ty_generics #where_clause {
#[allow(clippy::useless_format)]
fn validate(&self) -> Result<(), ModelValidatorError> {
#body_token
Ok(())
}
}
};
Ok(result)
}