use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Expr, Fields, Ident, Lit, Token,
};
fn ferro() -> TokenStream2 {
quote!(::ferro)
}
#[derive(Debug, Clone)]
struct ParsedRule {
name: String,
args: Vec<RuleArg>,
}
#[derive(Debug, Clone)]
enum RuleArg {
Int(i64),
Float(f64),
String(String),
Ident(String),
}
impl ToTokens for RuleArg {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
RuleArg::Int(n) => n.to_tokens(tokens),
RuleArg::Float(n) => n.to_tokens(tokens),
RuleArg::String(s) => s.to_tokens(tokens),
RuleArg::Ident(s) => {
let ident = quote::format_ident!("{}", s);
ident.to_tokens(tokens)
}
}
}
}
struct FieldRules {
name: String,
rules: Vec<ParsedRule>,
}
pub fn validate_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
&input,
"ValidateRules only supports named structs",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(&input, "ValidateRules only supports structs")
.to_compile_error()
.into();
}
};
let mut field_rules = Vec::new();
for field in fields {
let field_name = field.ident.as_ref().unwrap().to_string();
let rules = parse_field_rules(field);
if !rules.is_empty() {
field_rules.push(FieldRules {
name: field_name,
rules,
});
}
}
let ferro = ferro();
let validate_impl = generate_validate_impl(&ferro, &field_rules);
let rules_impl = generate_rules_impl(&ferro, &field_rules);
let expanded = quote! {
impl #ferro::validation::Validatable for #name
where
Self: ::serde::Serialize,
{
fn validate(&self) -> ::std::result::Result<(), #ferro::validation::ValidationError> {
#validate_impl
}
fn validation_rules() -> ::std::vec::Vec<(&'static str, ::std::vec::Vec<::std::boxed::Box<dyn #ferro::validation::Rule>>)> {
#rules_impl
}
}
};
TokenStream::from(expanded)
}
fn parse_field_rules(field: &syn::Field) -> Vec<ParsedRule> {
let mut rules = Vec::new();
for attr in &field.attrs {
if !attr.path().is_ident("rule") {
continue;
}
let result: Result<Punctuated<Expr, Token![,]>, _> =
attr.parse_args_with(Punctuated::parse_terminated);
if let Ok(exprs) = result {
for expr in exprs {
if let Some(rule) = parse_rule_expr(&expr) {
rules.push(rule);
}
}
}
}
rules
}
fn parse_rule_expr(expr: &Expr) -> Option<ParsedRule> {
match expr {
Expr::Path(path) => {
let name = path.path.get_ident()?.to_string();
Some(ParsedRule {
name,
args: Vec::new(),
})
}
Expr::Call(call) => {
let name = if let Expr::Path(path) = call.func.as_ref() {
path.path.get_ident()?.to_string()
} else {
return None;
};
let args: Vec<RuleArg> = call.args.iter().filter_map(parse_rule_arg).collect();
Some(ParsedRule { name, args })
}
_ => None,
}
}
fn parse_rule_arg(expr: &Expr) -> Option<RuleArg> {
match expr {
Expr::Lit(lit) => match &lit.lit {
Lit::Int(n) => Some(RuleArg::Int(n.base10_parse().ok()?)),
Lit::Float(n) => Some(RuleArg::Float(n.base10_parse().ok()?)),
Lit::Str(s) => Some(RuleArg::String(s.value())),
_ => None,
},
Expr::Path(path) => {
let ident = path.path.get_ident()?;
Some(RuleArg::Ident(ident.to_string()))
}
_ => None,
}
}
fn generate_validate_impl(ferro: &TokenStream2, field_rules: &[FieldRules]) -> TokenStream2 {
if field_rules.is_empty() {
return quote! { Ok(()) };
}
let mut field_validations = Vec::new();
for fr in field_rules {
let field_name = &fr.name;
let rule_calls: Vec<TokenStream2> = fr
.rules
.iter()
.map(|r| generate_rule_call(ferro, r))
.collect();
field_validations.push(quote! {
validator = validator.rules(#field_name, #ferro::rules![#(#rule_calls),*]);
});
}
quote! {
let data = #ferro::serde_json::to_value(self)
.map_err(|e| {
let mut err = #ferro::validation::ValidationError::new();
err.add("_struct", format!("Failed to serialize: {}", e));
err
})?;
let mut validator = #ferro::validation::Validator::new(&data);
#(#field_validations)*
validator.validate()
}
}
fn generate_rule_call(ferro: &TokenStream2, rule: &ParsedRule) -> TokenStream2 {
let rule_fn = Ident::new(&rule.name, proc_macro2::Span::call_site());
if rule.args.is_empty() {
quote! { #ferro::validation::#rule_fn() }
} else {
let args = &rule.args;
quote! { #ferro::validation::#rule_fn(#(#args),*) }
}
}
fn generate_rules_impl(ferro: &TokenStream2, field_rules: &[FieldRules]) -> TokenStream2 {
if field_rules.is_empty() {
return quote! { ::std::vec::Vec::new() };
}
let mut rule_entries = Vec::new();
for fr in field_rules {
let field_name = &fr.name;
let rule_calls: Vec<TokenStream2> = fr
.rules
.iter()
.map(|rule| {
let call = generate_rule_call(ferro, rule);
quote! {
::std::boxed::Box::new(#call) as ::std::boxed::Box<dyn #ferro::validation::Rule>
}
})
.collect();
rule_entries.push(quote! {
(#field_name, ::std::vec![#(#rule_calls),*])
});
}
quote! {
::std::vec![#(#rule_entries),*]
}
}