csv_schema_validator_derive/
lib.rs

1// csv-schema-validator-derive/src/lib.rs
2extern crate proc_macro;
3
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::{
7    parse_macro_input, Data, DeriveInput, Fields, GenericArgument, Ident,
8    PathArguments, Type,
9};
10
11// Armazena validações por campo
12struct FieldValidation {
13    field_name: Ident,
14    is_option: bool, // [FIX] passamos a carregar se o campo é Option<T>
15    validations: Vec<Validation>,
16    core_ty_ts: proc_macro2::TokenStream, // v.0.1.3: tipo efetivo (T de Option<T> ou o próprio) para codegen
17}
18
19// Tipos de validações suportadas
20enum Validation {
21    Range { min: Option<String>, max: Option<String>, is_float: bool }, // v.0.1.3
22    Regex { regex: String },
23    Required,
24    Custom { path: syn::Path },
25    Length { min: usize, max: usize },
26    NotBlank,
27    OneOf { values: Vec<String> },
28    NotIn { values: Vec<String> },    
29}
30
31impl Validation {
32    /// Faz o parse de #[validate(...)] em uma lista de Validation
33    fn parse_validations(input: syn::parse::ParseStream) -> syn::Result<Vec<Self>> {
34        let mut out = Vec::new();
35        let meta_items = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated(input)?;
36        for meta in meta_items {
37            match meta {
38                syn::Meta::Path(path) => Self::parse_meta_path(path, &mut out)?,
39                syn::Meta::NameValue(mnv) => Self::parse_meta_name_value(mnv, &mut out)?,
40                syn::Meta::List(list) => Self::parse_meta_list(list, &mut out)?,
41            }
42        }
43        Ok(out)
44    }
45
46    // ---------- Dispatchers por variante de Meta ----------
47
48    fn parse_meta_path(path: syn::Path, out: &mut Vec<Self>) -> syn::Result<()> {
49        if path.is_ident("required") {
50            out.push(Validation::Required);
51        } else if path.is_ident("not_blank") {
52            out.push(Validation::NotBlank);
53        }
54        Ok(())
55    }
56
57    fn parse_meta_name_value(mnv: syn::MetaNameValue, out: &mut Vec<Self>) -> syn::Result<()> {
58        if mnv.path.is_ident("regex") {
59            let s = Self::expect_lit_str(&mnv.value, "Expected string literal for `regex`")?;
60            out.push(Validation::Regex { regex: s });
61        } else if mnv.path.is_ident("custom") {
62            let s = Self::expect_lit_str(&mnv.value, "Expected string literal for `custom` (e.g., custom = \"path::to::fn\")")?;
63            let path: syn::Path = syn::parse_str(&s).map_err(|e| syn::Error::new_spanned(&mnv.value, e))?;
64            out.push(Validation::Custom { path });
65        }
66        Ok(())
67    }
68
69    fn parse_meta_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
70        let ident = &list.path;
71        if ident.is_ident("length") {
72            Self::parse_length_list(list, out)
73        } else if ident.is_ident("range") {
74            Self::parse_range_list(list, out)
75        } else if ident.is_ident("one_of") {
76            Self::parse_one_of_list(list, out)
77        } else if ident.is_ident("not_in") {
78            Self::parse_not_in_list(list, out)
79        } else {
80            Ok(())
81        }
82    }
83
84    // ---------- Handlers específicos ----------
85
86    fn parse_length_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
87        let items: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
88            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
89        let mut min: Option<usize> = None;
90        let mut max: Option<usize> = None;
91
92        for kv in items {
93            if kv.path.is_ident("min") {
94                let v = Self::expect_lit_int(&kv.value, "`min` for `length` must be an integer literal")?;
95                min = Some(v);
96            } else if kv.path.is_ident("max") {
97                let v = Self::expect_lit_int(&kv.value, "`max` for `length` must be an integer literal")?;
98                max = Some(v);
99            }
100        }
101
102        if min.is_none() && max.is_none() {
103            return Err(syn::Error::new_spanned(list, "`length` requires at least one of `min` or `max`"));
104        }
105        if let Some(mx) = max {
106            if mx == 0 {
107                return Err(syn::Error::new_spanned(list, "`max` for `length` cannot be zero"));
108            }
109        }
110        if let (Some(a), Some(b)) = (min, max) {
111            if a > b {
112                return Err(syn::Error::new_spanned(list, "`min` must be <= `max` for `length`"));
113            }
114        }
115
116        out.push(Validation::Length {
117            min: min.unwrap_or(0),
118            max: max.unwrap_or(usize::MAX),
119        });
120        Ok(())
121    }
122
123    fn parse_range_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
124        let items: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
125            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
126        let mut min: Option<String> = None;
127        let mut max: Option<String> = None;
128        let mut min_is_float = false;
129        let mut max_is_float = false;
130
131        for kv in items {
132            let (slot_val, slot_is_float) = if kv.path.is_ident("min") {
133                (&mut min, &mut min_is_float)
134            } else if kv.path.is_ident("max") {
135                (&mut max, &mut max_is_float)
136            } else {
137                continue;
138            };
139
140            match &kv.value {
141                // inteiros positivos
142                syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
143                    *slot_val = Some(i.to_string());
144                    *slot_is_float = false;
145                }
146                // floats positivos
147                syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
148                    *slot_val = Some(f.to_string());
149                    *slot_is_float = true;
150                }
151                // negativos: -<int> ou -<float>
152                syn::Expr::Unary(syn::ExprUnary { op: syn::UnOp::Neg(_), expr, .. }) => {
153                    match &**expr {
154                        syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
155                            *slot_val = Some(format!("-{}", i.to_string()));
156                            *slot_is_float = false;
157                        }
158                        syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
159                            *slot_val = Some(format!("-{}", f.to_string()));
160                            *slot_is_float = true;
161                        }
162                        _ => {
163                            return Err(syn::Error::new_spanned(
164                                &kv.value,
165                                "`range` values must be numeric literals (int or float)",
166                            ));
167                        }
168                    }
169                }
170                _ => {
171                    return Err(syn::Error::new_spanned(
172                        &kv.value,
173                        "`range` values must be numeric literals (int or float)",
174                    ));
175                }
176            }
177        }
178
179        if min.is_none() && max.is_none() {
180            return Err(syn::Error::new_spanned(
181                &list,
182                "`range` requires at least one of `min` or `max`",
183            ));
184        }
185
186        // Se ambos existem, precisam ser do mesmo "kind" (ambos int ou ambos float)
187        if min.is_some() && max.is_some() && (min_is_float != max_is_float) {
188            return Err(syn::Error::new_spanned(
189                &list,
190                "`range` `min` and `max` must be of the same type (both int or both float)",
191            ));
192        }
193
194        // Checagem min <= max quando ambos existem
195        if let (Some(ref a), Some(ref b)) = (&min, &max) {
196            if min_is_float {
197                // float
198                let av: f64 = a.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` float literal parse error"))?;
199                let bv: f64 = b.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` float literal parse error"))?;
200                if av > bv {
201                    return Err(syn::Error::new_spanned(&list, "`range` `min` must be <= `max`"));
202                }
203            } else {
204                // inteiro (suporta sinal)
205                let av: i128 = a.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` int literal parse error"))?;
206                let bv: i128 = b.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` int literal parse error"))?;
207                if av > bv {
208                    return Err(syn::Error::new_spanned(&list, "`range` `min` must be <= `max`"));
209                }
210            }
211        }
212
213        let is_float = min_is_float || max_is_float;
214
215        out.push(Validation::Range { min, max, is_float });
216        Ok(())
217    }
218
219    fn parse_one_of_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
220        let exprs: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]> =
221            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
222        let mut values = Vec::new();
223        for expr in exprs {
224            let s = Self::expect_lit_str_expr(expr, "`one_of` only accepts string literals")?;
225            values.push(s);
226        }
227        if values.is_empty() {
228            return Err(syn::Error::new_spanned(list, "`one_of` requires at least one value"));
229        }
230        out.push(Validation::OneOf { values });
231        Ok(())
232    }
233
234    fn parse_not_in_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
235        let exprs: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]> =
236            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
237        let mut values = Vec::new();
238        for expr in exprs {
239            let s = Self::expect_lit_str_expr(expr, "`not_in` only accepts string literals")?;
240            values.push(s);
241        }
242        if values.is_empty() {
243            return Err(syn::Error::new_spanned(list, "`not_in` requires at least one value"));
244        }
245        out.push(Validation::NotIn { values });
246        Ok(())
247    }
248
249    // ---------- Utilitários de extração ----------
250
251    fn expect_lit_str(expr: &syn::Expr, msg: &str) -> syn::Result<String> {
252        if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = expr {
253            Ok(s.value())
254        } else {
255            Err(syn::Error::new_spanned(expr, msg))
256        }
257    }
258
259    fn expect_lit_int(expr: &syn::Expr, msg: &str) -> syn::Result<usize> {
260        if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) = expr {
261            i.base10_parse::<usize>().map_err(|e| syn::Error::new_spanned(expr, e))
262        } else {
263            Err(syn::Error::new_spanned(expr, msg))
264        }
265    }
266
267    fn expect_lit_str_expr(expr: syn::Expr, msg: &str) -> syn::Result<String> {
268        if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = expr {
269            Ok(s.value())
270        } else {
271            Err(syn::Error::new_spanned(expr, msg))
272        }
273    }
274}
275
276
277// [FIX] helper para detectar Option<T>
278fn option_inner_type(ty: &Type) -> Option<&Type> {
279    if let Type::Path(tp) = ty {
280        if let Some(seg) = tp.path.segments.last() {
281            if seg.ident == "Option" {
282                if let PathArguments::AngleBracketed(args) = &seg.arguments {
283                    if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
284                        return Some(inner_ty);
285                    }
286                }
287            }
288        }
289    }
290    None
291}
292
293#[proc_macro_derive(ValidateCsv, attributes(validate))]
294pub fn validate_csv_derive(input: TokenStream) -> TokenStream {
295    let input = parse_macro_input!(input as DeriveInput);
296    let name = &input.ident;
297
298    let fields = match &input.data {
299        Data::Struct(data) => match &data.fields {
300            Fields::Named(f) => &f.named,
301            _ => {
302                return syn::Error::new_spanned(
303                    &data.fields,
304                    "only structs with named fields are supported",
305                )
306                .to_compile_error()
307                .into();
308            }
309        },
310        _ => {
311            return syn::Error::new_spanned(&input, "only structs are supported")
312                .to_compile_error()
313                .into();
314        }
315    };
316
317    let mut field_validations = Vec::new();
318
319    for field in fields {
320        let field_name = field.ident.as_ref().unwrap().clone();
321        let is_option = option_inner_type(&field.ty).is_some(); // [FIX] capturamos se é Option<T>
322        let mut validations = Vec::new();
323
324        for attr in &field.attrs {
325            if attr.path().is_ident("validate") {
326                match attr.parse_args_with(Validation::parse_validations) {
327                    Ok(mut v) => {
328                        // v.0.1.3: `required` só em Option<T>
329                        let has_required = v.iter().any(|vv| matches!(vv, Validation::Required)); // v.0.1.3
330                        if has_required && !is_option {                                          // v.0.1.3
331                            return syn::Error::new_spanned(
332                                &field.ty,
333                                format!("`required` can only be used on Option<T> fields (field `{}`)", field_name)
334                            ).to_compile_error().into();
335                        }
336
337                        // v.0.1.3: Restrições de tipo para validações baseadas em String
338                        let needs_string = v.iter().any(|vv| matches!(
339                            vv,
340                            Validation::Regex{..} | Validation::Length{..} | Validation::NotBlank
341                            | Validation::OneOf{..} | Validation::NotIn{..}
342                        )); // v.0.1.3
343                        if needs_string {
344                            let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); // v.0.1.3
345                            let ty_name = if let Type::Path(tp) = core_ty {                 // v.0.1.3
346                                tp.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default()
347                            } else { String::new() };                                        // v.0.1.3
348                            if ty_name != "String" {                                         // v.0.1.3
349                                return syn::Error::new_spanned(
350                                    core_ty,
351                                    format!("`regex`, `length`, `not_blank`, `one_of`, `not_in` require String (field `{}` is `{}`)", field_name, ty_name)
352                                ).to_compile_error().into();                                 // v.0.1.3
353                            }
354                        }
355
356                        // v.0.1.3: Restrições de tipo para `range`
357                        if let Some(is_float) = v.iter().find_map(|vv| {
358                            if let Validation::Range{is_float, ..} = vv { Some(*is_float) } else { None }
359                        }) { // v.0.1.3
360                            let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); // v.0.1.3
361                            let ty_name = if let Type::Path(tp) = core_ty {                  // v.0.1.3
362                                tp.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default()
363                            } else { String::new() };                                        // v.0.1.3
364                            let is_int = matches!(ty_name.as_str(),
365                                "i8"|"i16"|"i32"|"i64"|"i128"|"isize"|
366                                "u8"|"u16"|"u32"|"u64"|"u128"|"usize"
367                            ); // v.0.1.3
368                            let is_float_ty = matches!(ty_name.as_str(), "f32"|"f64"); // v.0.1.3
369                            if !(is_int || is_float_ty) {
370                                return syn::Error::new_spanned(
371                                    core_ty,
372                                    format!("`range` only applies to numeric fields (field `{}` is `{}`)", field_name, ty_name)
373                                ).to_compile_error().into(); // v.0.1.3
374                            }
375                            if is_float && !is_float_ty {
376                                return syn::Error::new_spanned(
377                                    core_ty,
378                                    format!("`range` with float literals requires float field (field `{}` is `{}`)", field_name, ty_name)
379                                ).to_compile_error().into(); // v.0.1.3
380                            }
381                            if !is_float && !is_int {
382                                return syn::Error::new_spanned(
383                                    core_ty,
384                                    format!("`range` with integer literals requires integer field (field `{}` is `{}`)", field_name, ty_name)
385                                ).to_compile_error().into(); // v.0.1.3
386                            }
387                        }
388
389                        validations.append(&mut v);
390                    },
391                    Err(e) => return e.to_compile_error().into(),
392                }
393            }
394        }
395
396        if !validations.is_empty() {
397            // v.0.1.3: calcular e guardar tipo efetivo para codegen (T se Option<T>, senão o próprio)
398            let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); // v.0.1.3
399            let core_ty_ts = quote! { #core_ty };                            // v.0.1.3
400
401            field_validations.push(FieldValidation {
402                field_name,
403                is_option,
404                validations,
405                core_ty_ts,   // v.0.1.3
406            });
407        }
408    }
409
410    let validation_arms = field_validations.into_iter().map(|fv| {
411        let field_name_str = fv.field_name.to_string();
412        let field_name_ident = fv.field_name;
413        let fv_is_option = fv.is_option;
414        let fv_core_ty_ts = fv.core_ty_ts.clone(); // v.0.1.3
415    
416        let checks = fv.validations.into_iter().map(|validation| {
417            match validation {
418                Validation::Required => {
419                    gen_required_check(&field_name_ident, &field_name_str)
420                }
421                Validation::NotBlank => {
422                    gen_not_blank_check(&field_name_ident, &field_name_str, fv_is_option)
423                }
424                Validation::Range { min, max, is_float: _ } => { // v.0.1.3
425                    gen_range_check(&field_name_ident, &field_name_str, fv_is_option, min, max, fv_core_ty_ts.clone()) // v.0.1.3
426                }
427                Validation::Length { min, max } => {
428                    gen_length_check(&field_name_ident, &field_name_str, fv_is_option, min, max)
429                }
430                Validation::Regex { regex } => {
431                    gen_regex_check(&field_name_ident, &field_name_str, fv_is_option, regex)
432                }
433                Validation::OneOf { values } => {
434                    gen_one_of_check(&field_name_ident, &field_name_str, fv_is_option, values)
435                }
436                Validation::NotIn { values } => {
437                    gen_not_in_check(&field_name_ident, &field_name_str, fv_is_option, values)
438                }
439                Validation::Custom { path } => {
440                    gen_custom_check(&field_name_ident, &field_name_str, fv_is_option, path)
441                }
442            }
443        });
444    
445        quote! { #(#checks)* }
446    });
447
448    let expanded = quote! {
449        impl #name {
450            pub fn validate_csv(&self) -> ::core::result::Result<(), ::std::vec::Vec<::csv_schema_validator::ValidationError>> {
451                let mut errors = ::std::vec::Vec::new();
452                #(#validation_arms)*
453                if errors.is_empty() {
454                    Ok(())
455                } else {
456                    Err(errors)
457                }
458            }
459        }
460    };
461
462    TokenStream::from(expanded)
463}
464
465use proc_macro2::TokenStream as TokenStream2;
466
467// Helpers por regra
468fn gen_required_check(field_ident: &syn::Ident, field_name: &str) -> TokenStream2 {
469    quote! {
470        if (&self.#field_ident).is_none() {
471            errors.push(::csv_schema_validator::ValidationError {
472                field: #field_name.to_string(),
473                message: "mandatory field".to_string(),
474            });
475        }
476    }
477}
478
479fn gen_not_blank_check(field_ident: &syn::Ident, field_name: &str, is_option: bool) -> TokenStream2 {
480    if is_option {
481        quote! {
482            if let Some(value) = &self.#field_ident {
483                if value.trim().is_empty() {
484                    errors.push(::csv_schema_validator::ValidationError {
485                        field: #field_name.to_string(),
486                        message: "must not be blank or contain only whitespace".to_string(),
487                    });
488                }
489            }
490        }
491    } else {
492        quote! {
493            let value = &self.#field_ident;
494            if value.trim().is_empty() {
495                errors.push(::csv_schema_validator::ValidationError {
496                    field: #field_name.to_string(),
497                    message: "must not be blank or contain only whitespace".to_string(),
498                });
499            }
500        }
501    }
502}
503
504// v.0.1.3: `range` agora recebe min/max como Option<String> e usa o tipo efetivo do campo;
505// tipamos os literais com o tipo do campo, para o compilador checar compatibilidade/overflow.
506// Também produzimos mensagens específicas quando ambos os limites existem.
507fn gen_range_check(
508    field_ident: &syn::Ident,
509    field_name: &str,
510    is_option: bool,
511    min: Option<String>,         // v.0.1.3
512    max: Option<String>,         // v.0.1.3
513    core_ty_ts: proc_macro2::TokenStream, // v.0.1.3
514) -> TokenStream2 {
515    let min_ts = min.as_ref().map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).expect("invalid min literal")); // v.0.1.3
516    let max_ts = max.as_ref().map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).expect("invalid max literal")); // v.0.1.3
517
518    let min_bind = min_ts.as_ref().map(|ts| quote! { let __csv_min: #core_ty_ts = #ts; }); // v.0.1.3
519    let max_bind = max_ts.as_ref().map(|ts| quote! { let __csv_max: #core_ty_ts = #ts; }); // v.0.1.3
520
521    // v.0.1.3: mensagens amigáveis (normalizamos sufixo ".0")
522    fn normalize_for_msg(s: &str) -> String { // v.0.1.3
523        if let Some(stripped) = s.strip_suffix(".0") { stripped.to_string() } else { s.to_string() }
524    }
525    let msg_between = match (min.as_ref(), max.as_ref()) { // v.0.1.3
526        (Some(a), Some(b)) => format!("value out of expected range: {} to {}", normalize_for_msg(a), normalize_for_msg(b)),
527        _ => "value out of expected range".to_string(),
528    };
529    let msg_below = match min.as_ref() { // v.0.1.3
530        Some(a) => format!("value below min: {}", normalize_for_msg(a)),
531        None => "value below min".to_string(),
532    };
533    let msg_above = match max.as_ref() { // v.0.1.3
534        Some(b) => format!("value above max: {}", normalize_for_msg(b)),
535        None => "value above max".to_string(),
536    };
537
538    let cmp = match (min_bind.is_some(), max_bind.is_some()) {
539        (true, true) => quote! {
540            if !(__csv_min <= *value && *value <= __csv_max) {
541                errors.push(::csv_schema_validator::ValidationError {
542                    field: #field_name.to_string(),
543                    message: #msg_between.to_string(), // v.0.1.3
544                });
545            }
546        },
547        (true, false) => quote! {
548            if !(__csv_min <= *value) {
549                errors.push(::csv_schema_validator::ValidationError {
550                    field: #field_name.to_string(),
551                    message: #msg_below.to_string(), // v.0.1.3
552                });
553            }
554        },
555        (false, true) => quote! {
556            if !(*value <= __csv_max) {
557                errors.push(::csv_schema_validator::ValidationError {
558                    field: #field_name.to_string(),
559                    message: #msg_above.to_string(), // v.0.1.3
560                });
561            }
562        },
563        _ => quote! {}, // impossível pois já validamos no parse
564    };
565
566    if is_option {
567        quote! {
568            { #min_bind #max_bind
569              if let Some(value) = &self.#field_ident { #cmp } }
570        }
571    } else {
572        quote! {
573            { #min_bind #max_bind
574              let value = &self.#field_ident; #cmp }
575        }
576    }
577}
578
579fn gen_length_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, min: usize, max: usize) -> TokenStream2 {
580    if is_option {
581        quote! {
582            if let Some(value) = &self.#field_ident {
583                let len = value.len();
584                if len < #min || len > #max {
585                    errors.push(::csv_schema_validator::ValidationError {
586                        field: #field_name.to_string(),
587                        message: format!("length out of expected range: {} to {}", #min, #max),
588                    });
589                }
590            }
591        }
592    } else {
593        quote! {
594            let value = &self.#field_ident;
595            let len = value.len();
596            if len < #min || len > #max {
597                errors.push(::csv_schema_validator::ValidationError {
598                    field: #field_name.to_string(),
599                    message: format!("length out of expected range: {} to {}", #min, #max),
600                });
601            }
602        }
603    }
604}
605
606fn gen_regex_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, regex: String) -> TokenStream2 {
607    let body = quote! {
608        use ::csv_schema_validator::__private::once_cell::sync::Lazy;
609        use ::csv_schema_validator::__private::regex;
610        static RE: Lazy<Result<regex::Regex, regex::Error>> = Lazy::new(|| regex::Regex::new(#regex));
611
612        match RE.as_ref() {
613            Ok(compiled_regex) => {
614                if !compiled_regex.is_match(value) {
615                    errors.push(::csv_schema_validator::ValidationError {
616                        field: #field_name.to_string(),
617                        message: "does not match the expected pattern".to_string(),
618                    });
619                }
620            }
621            Err(e) => {
622                errors.push(::csv_schema_validator::ValidationError {
623                    field: #field_name.to_string(),
624                    message: format!("invalid regex '{}': {}", #regex, e),
625                });
626            }
627        }
628    };
629    if is_option {
630        quote! {
631            if let Some(value) = &self.#field_ident {
632                #body
633            }
634        }
635    } else {
636        quote! {
637            let value = &self.#field_ident;
638            #body
639        }
640    }
641}
642
643fn gen_one_of_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, values: Vec<String>) -> TokenStream2 {
644    let arr = values; // mantém o padrão já usado
645    if is_option {
646        quote! {
647            if let Some(value) = &self.#field_ident {
648                const __ALLOWED: &[&str] = &[#(#arr),*];
649                if !__ALLOWED.contains(&value.as_str()) {
650                    errors.push(::csv_schema_validator::ValidationError {
651                        field: #field_name.to_string(),
652                        message: format!("invalid value"),
653                    });
654                }
655            }
656        }
657    } else {
658        quote! {
659            let value = &self.#field_ident;
660            const __ALLOWED: &[&str] = &[#(#arr),*];
661            if !__ALLOWED.contains(&value.as_str()) {
662                errors.push(::csv_schema_validator::ValidationError {
663                    field: #field_name.to_string(),
664                    message: format!("invalid value"),
665                });
666            }
667        }
668    }
669}
670
671fn gen_not_in_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, values: Vec<String>) -> TokenStream2 {
672    let arr = values;
673    if is_option {
674        quote! {
675            if let Some(value) = &self.#field_ident {
676                const __FORBIDDEN: &[&str] = &[#(#arr),*];
677                if __FORBIDDEN.contains(&value.as_str()) {
678                    errors.push(::csv_schema_validator::ValidationError {
679                        field: #field_name.to_string(),
680                        message: format!("value not allowed"),
681                    });
682                }
683            }
684        }
685    } else {
686        quote! {
687            let value = &self.#field_ident;
688            const __FORBIDDEN: &[&str] = &[#(#arr),*];
689            if __FORBIDDEN.contains(&value.as_str()) {
690                errors.push(::csv_schema_validator::ValidationError {
691                    field: #field_name.to_string(),
692                    message: format!("value not allowed"),
693                });
694            }
695        }
696    }
697}
698
699fn gen_custom_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, path: syn::Path) -> TokenStream2 {
700    if is_option {
701        quote! {
702            if let Some(value) = &self.#field_ident {
703                match #path(value) {
704                    Err(err) => {
705                        errors.push(::csv_schema_validator::ValidationError {
706                            field: #field_name.to_string(),
707                            message: format!("{}", err),
708                        });
709                    }
710                    Ok(()) => {}
711                }
712            }
713        }
714    } else {
715        quote! {
716            match #path(&self.#field_ident) {
717                Err(err) => {
718                    errors.push(::csv_schema_validator::ValidationError {
719                        field: #field_name.to_string(),
720                        message: format!("{}", err),
721                    });
722                }
723                Ok(()) => {}
724            }
725        }
726    }
727}