extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, Data, DeriveInput, Fields, GenericArgument, Ident,
PathArguments, Type,
};
struct FieldValidation {
field_name: Ident,
is_option: bool, validations: Vec<Validation>,
core_ty_ts: proc_macro2::TokenStream, }
enum Validation {
Range { min: Option<String>, max: Option<String>, is_float: bool }, Regex { regex: String },
Required,
Custom { path: syn::Path },
Length { min: usize, max: usize },
NotBlank,
OneOf { values: Vec<String> },
NotIn { values: Vec<String> },
IfThen { conditional_column: String, conditional_value: String, expected_value: String }, }
impl Validation {
fn parse_validations(input: syn::parse::ParseStream) -> syn::Result<Vec<Self>> {
let mut out = Vec::new();
let meta_items = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated(input)?;
for meta in meta_items {
match meta {
syn::Meta::Path(path) => Self::parse_meta_path(path, &mut out)?,
syn::Meta::NameValue(mnv) => Self::parse_meta_name_value(mnv, &mut out)?,
syn::Meta::List(list) => Self::parse_meta_list(list, &mut out)?,
}
}
Ok(out)
}
fn parse_meta_path(path: syn::Path, out: &mut Vec<Self>) -> syn::Result<()> {
if path.is_ident("required") {
out.push(Validation::Required);
} else if path.is_ident("not_blank") {
out.push(Validation::NotBlank);
}
Ok(())
}
fn parse_meta_name_value(mnv: syn::MetaNameValue, out: &mut Vec<Self>) -> syn::Result<()> {
if mnv.path.is_ident("regex") {
let s = Self::expect_lit_str(&mnv.value, "Expected string literal for `regex`")?;
out.push(Validation::Regex { regex: s });
Ok(())
} else if mnv.path.is_ident("custom") {
let s = Self::expect_lit_str(&mnv.value, "Expected string literal for `custom` (e.g., custom = \"path::to::fn\")")?;
let path: syn::Path = syn::parse_str(&s).map_err(|e| syn::Error::new_spanned(&mnv.value, e))?;
out.push(Validation::Custom { path });
Ok(())
} else {
Err(syn::Error::new_spanned(mnv, "chave desconhecida em atributo"))
}
}
fn parse_meta_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
let ident = &list.path;
if ident.is_ident("length") {
Self::parse_length_list(list, out)
} else if ident.is_ident("range") {
Self::parse_range_list(list, out)
} else if ident.is_ident("one_of") {
Self::parse_one_of_list(list, out)
} else if ident.is_ident("not_in") {
Self::parse_not_in_list(list, out)
} else if ident.is_ident("if_then") {
Self::parse_if_then_list(list, out)
} else {
Ok(())
}
}
fn parse_if_then_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
use syn::{LitStr, Token};
use syn::punctuated::Punctuated;
let args = list.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?;
if args.len() != 3 {
return Err(syn::Error::new_spanned(
list,
"if_then espera exatamente 3 strings: (conditional_column, conditional_value, expected_value)",
));
}
let conditional_column = args[0].value();
let conditional_value = args[1].value();
let expected_value = args[2].value();
out.push(Self::IfThen { conditional_column, conditional_value, expected_value });
Ok(())
}
fn parse_length_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
let items: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
let mut min: Option<usize> = None;
let mut max: Option<usize> = None;
for kv in items {
if kv.path.is_ident("min") {
let v = Self::expect_lit_int(&kv.value, "`min` for `length` must be an integer literal")?;
min = Some(v);
} else if kv.path.is_ident("max") {
let v = Self::expect_lit_int(&kv.value, "`max` for `length` must be an integer literal")?;
max = Some(v);
}
}
if min.is_none() && max.is_none() {
return Err(syn::Error::new_spanned(list, "`length` requires at least one of `min` or `max`"));
}
if let Some(mx) = max {
if mx == 0 {
return Err(syn::Error::new_spanned(list, "`max` for `length` cannot be zero"));
}
}
if let (Some(a), Some(b)) = (min, max) {
if a > b {
return Err(syn::Error::new_spanned(list, "`min` must be <= `max` for `length`"));
}
}
out.push(Validation::Length {
min: min.unwrap_or(0),
max: max.unwrap_or(usize::MAX),
});
Ok(())
}
fn parse_range_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
let items: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
let mut min: Option<String> = None;
let mut max: Option<String> = None;
let mut min_is_float = false;
let mut max_is_float = false;
for kv in items {
let (slot_val, slot_is_float) = if kv.path.is_ident("min") {
(&mut min, &mut min_is_float)
} else if kv.path.is_ident("max") {
(&mut max, &mut max_is_float)
} else {
continue;
};
match &kv.value {
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
*slot_val = Some(i.to_string());
*slot_is_float = false;
}
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
*slot_val = Some(f.to_string());
*slot_is_float = true;
}
syn::Expr::Unary(syn::ExprUnary { op: syn::UnOp::Neg(_), expr, .. }) => {
match &**expr {
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
*slot_val = Some(format!("-{}", i.to_string()));
*slot_is_float = false;
}
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
*slot_val = Some(format!("-{}", f.to_string()));
*slot_is_float = true;
}
_ => {
return Err(syn::Error::new_spanned(
&kv.value,
"`range` values must be numeric literals (int or float)",
));
}
}
}
_ => {
return Err(syn::Error::new_spanned(
&kv.value,
"`range` values must be numeric literals (int or float)",
));
}
}
}
if min.is_none() && max.is_none() {
return Err(syn::Error::new_spanned(
&list,
"`range` requires at least one of `min` or `max`",
));
}
if min.is_some() && max.is_some() && (min_is_float != max_is_float) {
return Err(syn::Error::new_spanned(
&list,
"`range` `min` and `max` must be of the same type (both int or both float)",
));
}
if let (Some(ref a), Some(ref b)) = (&min, &max) {
if min_is_float {
let av: f64 = a.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` float literal parse error"))?;
let bv: f64 = b.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` float literal parse error"))?;
if av > bv {
return Err(syn::Error::new_spanned(&list, "`range` `min` must be <= `max`"));
}
} else {
let av: i128 = a.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` int literal parse error"))?;
let bv: i128 = b.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` int literal parse error"))?;
if av > bv {
return Err(syn::Error::new_spanned(&list, "`range` `min` must be <= `max`"));
}
}
}
let is_float = min_is_float || max_is_float;
out.push(Validation::Range { min, max, is_float });
Ok(())
}
fn parse_one_of_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
let exprs: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]> =
list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
let mut values = Vec::new();
for expr in exprs {
let s = Self::expect_lit_str_expr(expr, "`one_of` only accepts string literals")?;
values.push(s);
}
if values.is_empty() {
return Err(syn::Error::new_spanned(list, "`one_of` requires at least one value"));
}
out.push(Validation::OneOf { values });
Ok(())
}
fn parse_not_in_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
let exprs: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]> =
list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
let mut values = Vec::new();
for expr in exprs {
let s = Self::expect_lit_str_expr(expr, "`not_in` only accepts string literals")?;
values.push(s);
}
if values.is_empty() {
return Err(syn::Error::new_spanned(list, "`not_in` requires at least one value"));
}
out.push(Validation::NotIn { values });
Ok(())
}
fn type_name_of(ty: &Type) -> String {
if let Type::Path(tp) = ty {
tp.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default()
} else { String::new() }
}
fn is_int_ty(n: &str) -> bool {
matches!(n, "i8"|"i16"|"i32"|"i64"|"i128"|"isize"|
"u8"|"u16"|"u32"|"u64"|"u128"|"usize")
}
fn is_float_ty(n: &str) -> bool {
matches!(n, "f32"|"f64")
}
fn validate_if_then_for_field(
v: &[Validation],
field: &syn::Field,
field_name: &syn::Ident,
is_option: bool,
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> Result<(), proc_macro::TokenStream> {
let Some((conditional_column, conditional_value, expected_value)) =
v.iter().find_map(|vv| {
if let Validation::IfThen { conditional_column, conditional_value, expected_value } = vv {
Some((conditional_column.as_str(), conditional_value.as_str(), expected_value.as_str()))
} else {
None
}
})
else {
return Ok(());
};
if !is_option {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then` só pode ser usado em campos Option<T> (campo `{}`)", field_name)
).to_compile_error().into()
);
}
let Some(cond_field) = fields.iter().find(|f|
f.ident.as_ref().map(|i| i.to_string() == conditional_column).unwrap_or(false)
) else {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: campo condicional `{}` não existe na struct", conditional_column)
).to_compile_error().into()
);
};
let Some(cond_core_ty) = option_inner_type(&cond_field.ty) else {
return Err(
syn::Error::new_spanned(
&cond_field.ty,
format!("`if_then`: campo condicional `{}` deve ser Option<U>", conditional_column)
).to_compile_error().into()
);
};
let cond_ty_name = Self::type_name_of(cond_core_ty); let Some(target_core_ty) = option_inner_type(&field.ty) else { unreachable!() }; let target_ty_name = Self::type_name_of(target_core_ty);
if cond_ty_name != "String" {
if Self::is_int_ty(&cond_ty_name) {
if conditional_value.parse::<i128>().is_err() && conditional_value.parse::<u128>().is_err() {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: `conditional_value`='{}' inválido para tipo {}", conditional_value, cond_ty_name)
).to_compile_error().into()
);
}
} else if Self::is_float_ty(&cond_ty_name) {
if conditional_value.parse::<f64>().is_err() {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: `conditional_value`='{}' inválido para tipo {}", conditional_value, cond_ty_name)
).to_compile_error().into()
);
}
} else if cond_ty_name == "bool" {
if conditional_value.parse::<bool>().is_err() {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: `conditional_value`='{}' inválido para tipo bool (use 'true' ou 'false')", conditional_value)
).to_compile_error().into()
);
}
} else {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: tipo condicional `{}` não suportado; use String ou numérico", cond_ty_name)
).to_compile_error().into()
);
}
}
if target_ty_name != "String" {
if Self::is_int_ty(&target_ty_name) {
if expected_value.parse::<i128>().is_err() && expected_value.parse::<u128>().is_err() {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: `expected_value`='{}' inválido para tipo {}", expected_value, target_ty_name)
).to_compile_error().into()
);
}
} else if Self::is_float_ty(&target_ty_name) {
if expected_value.parse::<f64>().is_err() {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: `expected_value`='{}' inválido para tipo {}", expected_value, target_ty_name)
).to_compile_error().into()
);
}
} else if target_ty_name == "bool" {
if expected_value.parse::<bool>().is_err() {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: `expected_value`='{}' inválido para tipo bool (use 'true' ou 'false')", expected_value)
).to_compile_error().into()
);
}
} else {
return Err(
syn::Error::new_spanned(
&field.ty,
format!("`if_then`: tipo do campo `{}` não suportado; use String ou numérico", target_ty_name)
).to_compile_error().into()
);
}
}
Ok(())
}
fn expect_lit_str(expr: &syn::Expr, msg: &str) -> syn::Result<String> {
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = expr {
Ok(s.value())
} else {
Err(syn::Error::new_spanned(expr, msg))
}
}
fn expect_lit_int(expr: &syn::Expr, msg: &str) -> syn::Result<usize> {
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) = expr {
i.base10_parse::<usize>().map_err(|e| syn::Error::new_spanned(expr, e))
} else {
Err(syn::Error::new_spanned(expr, msg))
}
}
fn expect_lit_str_expr(expr: syn::Expr, msg: &str) -> syn::Result<String> {
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = expr {
Ok(s.value())
} else {
Err(syn::Error::new_spanned(expr, msg))
}
}
}
fn option_inner_type(ty: &Type) -> Option<&Type> {
if let Type::Path(tp) = ty {
if let Some(seg) = tp.path.segments.last() {
if seg.ident == "Option" {
if let PathArguments::AngleBracketed(args) = &seg.arguments {
if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
return Some(inner_ty);
}
}
}
}
}
None
}
#[proc_macro_derive(ValidateCsv, attributes(validate))]
pub fn validate_csv_derive(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(f) => &f.named,
_ => {
return syn::Error::new_spanned(
&data.fields,
"only structs with named fields are supported",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(&input, "only structs are supported")
.to_compile_error()
.into();
}
};
let mut field_validations = Vec::new();
for field in fields {
let field_name = field.ident.as_ref().unwrap().clone();
let is_option = option_inner_type(&field.ty).is_some(); let mut validations = Vec::new();
for attr in &field.attrs {
if attr.path().is_ident("validate") {
match attr.parse_args_with(Validation::parse_validations) {
Ok(mut v) => {
let has_required = v.iter().any(|vv| matches!(vv, Validation::Required)); if has_required && !is_option { return syn::Error::new_spanned(
&field.ty,
format!("`required` can only be used on Option<T> fields (field `{}`)", field_name)
).to_compile_error().into();
}
let needs_string = v.iter().any(|vv| matches!(
vv,
Validation::Regex{..} | Validation::Length{..} | Validation::NotBlank
| Validation::OneOf{..} | Validation::NotIn{..}
)); if needs_string {
let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); let ty_name = Validation::type_name_of(core_ty); if ty_name != "String" { return syn::Error::new_spanned(
core_ty,
format!("`regex`, `length`, `not_blank`, `one_of`, `not_in` require String (field `{}` is `{}`)", field_name, ty_name)
).to_compile_error().into(); }
}
if let Some(is_float) = v.iter().find_map(|vv| {
if let Validation::Range{is_float, ..} = vv { Some(*is_float) } else { None }
}) { let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); let ty_name = Validation::type_name_of(core_ty);
let is_int = Validation::is_int_ty(&ty_name);
let is_float_ty = Validation::is_float_ty(&ty_name);
if !(is_int || is_float_ty) {
return syn::Error::new_spanned(
core_ty,
format!("`range` only applies to numeric fields (field `{}` is `{}`)", field_name, ty_name)
).to_compile_error().into(); }
if is_float && !is_float_ty {
return syn::Error::new_spanned(
core_ty,
format!("`range` with float literals requires float field (field `{}` is `{}`)", field_name, ty_name)
).to_compile_error().into(); }
if !is_float && !is_int {
return syn::Error::new_spanned(
core_ty,
format!("`range` with integer literals requires integer field (field `{}` is `{}`)", field_name, ty_name)
).to_compile_error().into(); }
}
if let Err(ts) = Validation::validate_if_then_for_field(&v, field, &field_name, is_option, fields) {
return ts; }
validations.append(&mut v);
},
Err(e) => return e.to_compile_error().into(),
}
}
}
if !validations.is_empty() {
let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); let core_ty_ts = quote! { #core_ty };
field_validations.push(FieldValidation {
field_name,
is_option,
validations,
core_ty_ts, });
}
}
let validation_arms = field_validations.into_iter().map(|fv| {
let field_name_str = fv.field_name.to_string();
let field_name_ident = fv.field_name;
let fv_is_option = fv.is_option;
let fv_core_ty_ts = fv.core_ty_ts.clone();
let checks = fv.validations.into_iter().map(|validation| {
match validation {
Validation::Required => {
gen_required_check(&field_name_ident, &field_name_str)
}
Validation::NotBlank => {
gen_not_blank_check(&field_name_ident, &field_name_str, fv_is_option)
}
Validation::Range { min, max, is_float: _ } => { gen_range_check(&field_name_ident, &field_name_str, fv_is_option, min, max, fv_core_ty_ts.clone()) }
Validation::Length { min, max } => {
gen_length_check(&field_name_ident, &field_name_str, fv_is_option, min, max)
}
Validation::Regex { regex } => {
gen_regex_check(&field_name_ident, &field_name_str, fv_is_option, regex)
}
Validation::OneOf { values } => {
gen_one_of_check(&field_name_ident, &field_name_str, fv_is_option, values)
}
Validation::NotIn { values } => {
gen_not_in_check(&field_name_ident, &field_name_str, fv_is_option, values)
}
Validation::Custom { path } => {
gen_custom_check(&field_name_ident, &field_name_str, fv_is_option, path)
}
Validation::IfThen { conditional_column, conditional_value, expected_value } => {
gen_if_then(
&field_name_ident,
&field_name_str,
fv_is_option,
fv_core_ty_ts.clone(),
conditional_column,
conditional_value,
expected_value,
)
} }
});
quote! { #(#checks)* }
});
let expanded = quote! {
impl #name {
pub fn validate_csv(&self) -> ::core::result::Result<(), ::std::vec::Vec<::csv_schema_validator::ValidationError>> {
let mut errors = ::std::vec::Vec::new();
#(#validation_arms)*
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
};
TokenStream::from(expanded)
}
use proc_macro2::TokenStream as TokenStream2;
fn gen_required_check(field_ident: &syn::Ident, field_name: &str) -> TokenStream2 {
quote! {
if (&self.#field_ident).is_none() {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: "mandatory field".to_string(),
});
}
}
}
fn gen_not_blank_check(field_ident: &syn::Ident, field_name: &str, is_option: bool) -> TokenStream2 {
if is_option {
quote! {
if let Some(value) = &self.#field_ident {
if value.trim().is_empty() {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: "must not be blank or contain only whitespace".to_string(),
});
}
}
}
} else {
quote! {
let value = &self.#field_ident;
if value.trim().is_empty() {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: "must not be blank or contain only whitespace".to_string(),
});
}
}
}
}
fn gen_range_check(
field_ident: &syn::Ident,
field_name: &str,
is_option: bool,
min: Option<String>, max: Option<String>, core_ty_ts: proc_macro2::TokenStream, ) -> TokenStream2 {
let min_ts = min.as_ref().map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).expect("invalid min literal")); let max_ts = max.as_ref().map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).expect("invalid max literal"));
let min_bind = min_ts.as_ref().map(|ts| quote! { let __csv_min: #core_ty_ts = #ts; }); let max_bind = max_ts.as_ref().map(|ts| quote! { let __csv_max: #core_ty_ts = #ts; });
fn normalize_for_msg(s: &str) -> String { if let Some(stripped) = s.strip_suffix(".0") { stripped.to_string() } else { s.to_string() }
}
let msg_between = match (min.as_ref(), max.as_ref()) { (Some(a), Some(b)) => format!("value out of expected range: {} to {}", normalize_for_msg(a), normalize_for_msg(b)),
_ => "value out of expected range".to_string(),
};
let msg_below = match min.as_ref() { Some(a) => format!("value below min: {}", normalize_for_msg(a)),
None => "value below min".to_string(),
};
let msg_above = match max.as_ref() { Some(b) => format!("value above max: {}", normalize_for_msg(b)),
None => "value above max".to_string(),
};
let cmp = match (min_bind.is_some(), max_bind.is_some()) {
(true, true) => quote! {
if !(__csv_min <= *value && *value <= __csv_max) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: #msg_between.to_string(), });
}
},
(true, false) => quote! {
if !(__csv_min <= *value) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: #msg_below.to_string(), });
}
},
(false, true) => quote! {
if !(*value <= __csv_max) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: #msg_above.to_string(), });
}
},
_ => quote! {}, };
if is_option {
quote! {
{ #min_bind #max_bind
if let Some(value) = &self.#field_ident { #cmp } }
}
} else {
quote! {
{ #min_bind #max_bind
let value = &self.#field_ident; #cmp }
}
}
}
fn gen_length_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, min: usize, max: usize) -> TokenStream2 {
if is_option {
quote! {
if let Some(value) = &self.#field_ident {
let len = value.len();
if len < #min || len > #max {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("length out of expected range: {} to {}", #min, #max),
});
}
}
}
} else {
quote! {
let value = &self.#field_ident;
let len = value.len();
if len < #min || len > #max {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("length out of expected range: {} to {}", #min, #max),
});
}
}
}
}
fn gen_regex_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, regex: String) -> TokenStream2 {
let body = quote! {
use ::csv_schema_validator::__private::once_cell::sync::Lazy;
use ::csv_schema_validator::__private::regex;
static RE: Lazy<Result<regex::Regex, regex::Error>> = Lazy::new(|| regex::Regex::new(#regex));
match RE.as_ref() {
Ok(compiled_regex) => {
if !compiled_regex.is_match(value) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: "does not match the expected pattern".to_string(),
});
}
}
Err(e) => {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("invalid regex '{}': {}", #regex, e),
});
}
}
};
if is_option {
quote! {
if let Some(value) = &self.#field_ident {
#body
}
}
} else {
quote! {
let value = &self.#field_ident;
#body
}
}
}
fn gen_one_of_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, values: Vec<String>) -> TokenStream2 {
let arr = values; if is_option {
quote! {
if let Some(value) = &self.#field_ident {
const __ALLOWED: &[&str] = &[#(#arr),*];
if !__ALLOWED.contains(&value.as_str()) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("invalid value"),
});
}
}
}
} else {
quote! {
let value = &self.#field_ident;
const __ALLOWED: &[&str] = &[#(#arr),*];
if !__ALLOWED.contains(&value.as_str()) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("invalid value"),
});
}
}
}
}
fn gen_not_in_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, values: Vec<String>) -> TokenStream2 {
let arr = values;
if is_option {
quote! {
if let Some(value) = &self.#field_ident {
const __FORBIDDEN: &[&str] = &[#(#arr),*];
if __FORBIDDEN.contains(&value.as_str()) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("value not allowed"),
});
}
}
}
} else {
quote! {
let value = &self.#field_ident;
const __FORBIDDEN: &[&str] = &[#(#arr),*];
if __FORBIDDEN.contains(&value.as_str()) {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("value not allowed"),
});
}
}
}
}
fn gen_custom_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, path: syn::Path) -> TokenStream2 {
if is_option {
quote! {
if let Some(value) = &self.#field_ident {
match #path(value) {
Err(err) => {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("{}", err),
});
}
Ok(()) => {}
}
}
}
} else {
quote! {
match #path(&self.#field_ident) {
Err(err) => {
errors.push(::csv_schema_validator::ValidationError {
field: #field_name.to_string(),
message: format!("{}", err),
});
}
Ok(()) => {}
}
}
}
}
fn gen_if_then(
target_field_ident: &syn::Ident, target_field_name: &str, _target_is_option: bool, target_core_ty_ts: proc_macro2::TokenStream, conditional_column: String, conditional_value: String, expected_value: String, ) -> TokenStream2 {
let cond_ident = syn::Ident::new(&conditional_column, proc_macro2::Span::call_site());
let cond_val_str = conditional_value;
let expected_val_str = expected_value;
quote! {
{
let __csv_expected_parse: ::core::result::Result<#target_core_ty_ts, _> =
<#target_core_ty_ts as ::core::str::FromStr>::from_str(#expected_val_str);
if let Ok(__csv_expected) = __csv_expected_parse {
#[inline]
fn __csv_eq_parsed<T>(cond_ref: &T, s: &str) -> bool
where
T: ::core::str::FromStr + ::core::cmp::PartialEq,
{
match <T as ::core::str::FromStr>::from_str(s) {
Ok(v) => *cond_ref == v,
Err(_) => false,
}
}
let __csv_condition_holds = match &self.#cond_ident {
Some(__cond_ref) => __csv_eq_parsed(__cond_ref, #cond_val_str),
None => false,
};
if __csv_condition_holds {
match &self.#target_field_ident {
Some(__v) if *__v == __csv_expected => { }
Some(_) => {
errors.push(::csv_schema_validator::ValidationError {
field: #target_field_name.to_string(),
message: format!(
"must be {} when {} == {}",
#expected_val_str, #conditional_column, #cond_val_str
),
});
}
None => {
errors.push(::csv_schema_validator::ValidationError {
field: #target_field_name.to_string(),
message: format!(
"must be {} when {} == {} (missing value)",
#expected_val_str, #conditional_column, #cond_val_str
),
});
}
}
}
} else {
errors.push(::csv_schema_validator::ValidationError {
field: #target_field_name.to_string(),
message: format!(
"invalid expected_value '{}' for field type",
#expected_val_str
),
});
}
}
}
}