roadblk-expand 0.1.6

Validator proc-macro expand impl
Documentation
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 DescAttr {
    Desc(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 = DescAttr::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 DescAttr::Desc(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_name: #field_name,
                            field_desc: #field_desc,
                        });
                    }
                },
                None => quote! {
                    #validate_token
                    if let Err(e) = validated {
                        return Err(ModelValidatorError {
                            error_ty: Box::new(e.into()),
                            error_message: None,
                            field_name: #field_name,
                            field_desc: #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)
}