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    IfThen { conditional_column: String, conditional_value: String, expected_value: String }, // v 0.2.0 
30}
31
32impl Validation {
33    /// Faz o parse de #[validate(...)] em uma lista de Validation
34    fn parse_validations(input: syn::parse::ParseStream) -> syn::Result<Vec<Self>> {
35        let mut out = Vec::new();
36        let meta_items = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated(input)?;
37        for meta in meta_items {
38            match meta {
39                syn::Meta::Path(path) => Self::parse_meta_path(path, &mut out)?,
40                syn::Meta::NameValue(mnv) => Self::parse_meta_name_value(mnv, &mut out)?,
41                syn::Meta::List(list) => Self::parse_meta_list(list, &mut out)?,
42            }
43        }
44        Ok(out)
45    }
46
47    // ---------- Dispatchers por variante de Meta ----------
48
49    fn parse_meta_path(path: syn::Path, out: &mut Vec<Self>) -> syn::Result<()> {
50        if path.is_ident("required") {
51            out.push(Validation::Required);
52        } else if path.is_ident("not_blank") {
53            out.push(Validation::NotBlank);
54        }
55        Ok(())
56    }
57
58    fn parse_meta_name_value(mnv: syn::MetaNameValue, out: &mut Vec<Self>) -> syn::Result<()> {
59        if mnv.path.is_ident("regex") {
60            let s = Self::expect_lit_str(&mnv.value, "Expected string literal for `regex`")?;
61            out.push(Validation::Regex { regex: s });
62            Ok(())
63        } else if mnv.path.is_ident("custom") {
64            let s = Self::expect_lit_str(&mnv.value, "Expected string literal for `custom` (e.g., custom = \"path::to::fn\")")?;
65            let path: syn::Path = syn::parse_str(&s).map_err(|e| syn::Error::new_spanned(&mnv.value, e))?;
66            out.push(Validation::Custom { path });
67            Ok(())
68        } else {
69            Err(syn::Error::new_spanned(mnv, "chave desconhecida em atributo"))
70        }
71    }
72
73    fn parse_meta_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
74        let ident = &list.path;
75        if ident.is_ident("length") {
76            Self::parse_length_list(list, out)
77        } else if ident.is_ident("range") {
78            Self::parse_range_list(list, out)
79        } else if ident.is_ident("one_of") {
80            Self::parse_one_of_list(list, out)
81        } else if ident.is_ident("not_in") {
82            Self::parse_not_in_list(list, out)
83        } else if ident.is_ident("if_then") {
84            Self::parse_if_then_list(list, out)
85        } else {
86            Ok(())
87        }
88    }
89
90    // ---------- Handlers específicos ----------
91
92    fn parse_if_then_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
93        use syn::{LitStr, Token};
94        use syn::punctuated::Punctuated;
95
96        // Espera exatamente 3 literais string
97        let args = list.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?;
98        if args.len() != 3 {
99            return Err(syn::Error::new_spanned(
100                list,
101                "if_then espera exatamente 3 strings: (conditional_column, conditional_value, expected_value)",
102            ));
103        }
104
105        let conditional_column = args[0].value();
106        let conditional_value  = args[1].value();
107        let expected_value     = args[2].value();
108
109        out.push(Self::IfThen { conditional_column, conditional_value, expected_value });
110        Ok(())
111    }
112
113    fn parse_length_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
114        let items: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
115            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
116        let mut min: Option<usize> = None;
117        let mut max: Option<usize> = None;
118
119        for kv in items {
120            if kv.path.is_ident("min") {
121                let v = Self::expect_lit_int(&kv.value, "`min` for `length` must be an integer literal")?;
122                min = Some(v);
123            } else if kv.path.is_ident("max") {
124                let v = Self::expect_lit_int(&kv.value, "`max` for `length` must be an integer literal")?;
125                max = Some(v);
126            }
127        }
128
129        if min.is_none() && max.is_none() {
130            return Err(syn::Error::new_spanned(list, "`length` requires at least one of `min` or `max`"));
131        }
132        if let Some(mx) = max {
133            if mx == 0 {
134                return Err(syn::Error::new_spanned(list, "`max` for `length` cannot be zero"));
135            }
136        }
137        if let (Some(a), Some(b)) = (min, max) {
138            if a > b {
139                return Err(syn::Error::new_spanned(list, "`min` must be <= `max` for `length`"));
140            }
141        }
142
143        out.push(Validation::Length {
144            min: min.unwrap_or(0),
145            max: max.unwrap_or(usize::MAX),
146        });
147        Ok(())
148    }
149
150    fn parse_range_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
151        let items: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
152            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
153        let mut min: Option<String> = None;
154        let mut max: Option<String> = None;
155        let mut min_is_float = false;
156        let mut max_is_float = false;
157
158        for kv in items {
159            let (slot_val, slot_is_float) = if kv.path.is_ident("min") {
160                (&mut min, &mut min_is_float)
161            } else if kv.path.is_ident("max") {
162                (&mut max, &mut max_is_float)
163            } else {
164                continue;
165            };
166
167            match &kv.value {
168                // inteiros positivos
169                syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
170                    *slot_val = Some(i.to_string());
171                    *slot_is_float = false;
172                }
173                // floats positivos
174                syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
175                    *slot_val = Some(f.to_string());
176                    *slot_is_float = true;
177                }
178                // negativos: -<int> ou -<float>
179                syn::Expr::Unary(syn::ExprUnary { op: syn::UnOp::Neg(_), expr, .. }) => {
180                    match &**expr {
181                        syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
182                            *slot_val = Some(format!("-{}", i.to_string()));
183                            *slot_is_float = false;
184                        }
185                        syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
186                            *slot_val = Some(format!("-{}", f.to_string()));
187                            *slot_is_float = true;
188                        }
189                        _ => {
190                            return Err(syn::Error::new_spanned(
191                                &kv.value,
192                                "`range` values must be numeric literals (int or float)",
193                            ));
194                        }
195                    }
196                }
197                _ => {
198                    return Err(syn::Error::new_spanned(
199                        &kv.value,
200                        "`range` values must be numeric literals (int or float)",
201                    ));
202                }
203            }
204        }
205
206        if min.is_none() && max.is_none() {
207            return Err(syn::Error::new_spanned(
208                &list,
209                "`range` requires at least one of `min` or `max`",
210            ));
211        }
212
213        // Se ambos existem, precisam ser do mesmo "kind" (ambos int ou ambos float)
214        if min.is_some() && max.is_some() && (min_is_float != max_is_float) {
215            return Err(syn::Error::new_spanned(
216                &list,
217                "`range` `min` and `max` must be of the same type (both int or both float)",
218            ));
219        }
220
221        // Checagem min <= max quando ambos existem
222        if let (Some(ref a), Some(ref b)) = (&min, &max) {
223            if min_is_float {
224                // float
225                let av: f64 = a.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` float literal parse error"))?;
226                let bv: f64 = b.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` float literal parse error"))?;
227                if av > bv {
228                    return Err(syn::Error::new_spanned(&list, "`range` `min` must be <= `max`"));
229                }
230            } else {
231                // inteiro (suporta sinal)
232                let av: i128 = a.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` int literal parse error"))?;
233                let bv: i128 = b.parse().map_err(|_| syn::Error::new_spanned(&list, "`range` int literal parse error"))?;
234                if av > bv {
235                    return Err(syn::Error::new_spanned(&list, "`range` `min` must be <= `max`"));
236                }
237            }
238        }
239
240        let is_float = min_is_float || max_is_float;
241
242        out.push(Validation::Range { min, max, is_float });
243        Ok(())
244    }
245
246    fn parse_one_of_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
247        let exprs: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]> =
248            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
249        let mut values = Vec::new();
250        for expr in exprs {
251            let s = Self::expect_lit_str_expr(expr, "`one_of` only accepts string literals")?;
252            values.push(s);
253        }
254        if values.is_empty() {
255            return Err(syn::Error::new_spanned(list, "`one_of` requires at least one value"));
256        }
257        out.push(Validation::OneOf { values });
258        Ok(())
259    }
260
261    fn parse_not_in_list(list: syn::MetaList, out: &mut Vec<Self>) -> syn::Result<()> {
262        let exprs: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]> =
263            list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
264        let mut values = Vec::new();
265        for expr in exprs {
266            let s = Self::expect_lit_str_expr(expr, "`not_in` only accepts string literals")?;
267            values.push(s);
268        }
269        if values.is_empty() {
270            return Err(syn::Error::new_spanned(list, "`not_in` requires at least one value"));
271        }
272        out.push(Validation::NotIn { values });
273        Ok(())
274    }
275
276    // ---------- UTILITÁRIOS GERAIS ----------
277
278    fn type_name_of(ty: &Type) -> String {
279        if let Type::Path(tp) = ty {
280            tp.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default()
281        } else { String::new() }
282    }
283
284    fn is_int_ty(n: &str) -> bool {
285        matches!(n, "i8"|"i16"|"i32"|"i64"|"i128"|"isize"|
286                    "u8"|"u16"|"u32"|"u64"|"u128"|"usize")
287    }
288
289    fn is_float_ty(n: &str) -> bool {
290        matches!(n, "f32"|"f64")
291    }
292
293    // ---------- FUNÇÕES AUXILIARES DE VALIDAÇÃO ----------
294
295    fn validate_if_then_for_field(
296        v: &[Validation],
297        field: &syn::Field,
298        field_name: &syn::Ident,
299        is_option: bool,
300        fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
301    ) -> Result<(), proc_macro::TokenStream> {
302        // Procura um if_then neste campo (se houver vários, valida o primeiro; repita se quiser todos)
303        let Some((conditional_column, conditional_value, expected_value)) =
304            v.iter().find_map(|vv| {
305                if let Validation::IfThen { conditional_column, conditional_value, expected_value } = vv {
306                    Some((conditional_column.as_str(), conditional_value.as_str(), expected_value.as_str()))
307                } else {
308                    None
309                }
310            })
311        else {
312            return Ok(());
313        };
314        
315
316        // 1) Campo alvo deve ser Option<_>
317        if !is_option {
318            return Err(
319                syn::Error::new_spanned(
320                    &field.ty,
321                    format!("`if_then` só pode ser usado em campos Option<T> (campo `{}`)", field_name)
322                ).to_compile_error().into()
323            );
324        }
325
326        // 2) Coluna condicional: existe e é Option<_>
327        let Some(cond_field) = fields.iter().find(|f|
328            f.ident.as_ref().map(|i| i.to_string() == conditional_column).unwrap_or(false)        
329        ) else {
330            return Err(
331                syn::Error::new_spanned(
332                    &field.ty,
333                    format!("`if_then`: campo condicional `{}` não existe na struct", conditional_column)
334                ).to_compile_error().into()
335            );
336        };
337
338        let Some(cond_core_ty) = option_inner_type(&cond_field.ty) else {
339            return Err(
340                syn::Error::new_spanned(
341                    &cond_field.ty,
342                    format!("`if_then`: campo condicional `{}` deve ser Option<U>", conditional_column)
343                ).to_compile_error().into()
344            );
345        };
346
347        // Tipos base
348        let cond_ty_name   = Self::type_name_of(cond_core_ty);                          // T
349        let Some(target_core_ty) = option_inner_type(&field.ty) else { unreachable!() }; // R
350        let target_ty_name = Self::type_name_of(target_core_ty);                         // R
351
352        // 3) Compatibilidade de literais (checagem leve)
353        if cond_ty_name != "String" {
354            if Self::is_int_ty(&cond_ty_name) {
355                if conditional_value.parse::<i128>().is_err() && conditional_value.parse::<u128>().is_err() {
356                    return Err(
357                        syn::Error::new_spanned(
358                            &field.ty,
359                            format!("`if_then`: `conditional_value`='{}' inválido para tipo {}", conditional_value, cond_ty_name)
360                        ).to_compile_error().into()
361                    );
362                }
363            } else if Self::is_float_ty(&cond_ty_name) {
364                if conditional_value.parse::<f64>().is_err() {
365                    return Err(
366                        syn::Error::new_spanned(
367                            &field.ty,
368                            format!("`if_then`: `conditional_value`='{}' inválido para tipo {}", conditional_value, cond_ty_name)
369                        ).to_compile_error().into()
370                    );
371                }
372            } else if cond_ty_name == "bool" {
373                if conditional_value.parse::<bool>().is_err() {
374                    return Err(
375                        syn::Error::new_spanned(
376                            &field.ty,
377                            format!("`if_then`: `conditional_value`='{}' inválido para tipo bool (use 'true' ou 'false')", conditional_value)
378                        ).to_compile_error().into()
379                    );
380                }
381            } else {
382                return Err(
383                    syn::Error::new_spanned(
384                        &field.ty,
385                        format!("`if_then`: tipo condicional `{}` não suportado; use String ou numérico", cond_ty_name)
386                    ).to_compile_error().into()
387                );
388            }
389        }
390
391        if target_ty_name != "String" {
392            if Self::is_int_ty(&target_ty_name) {
393                if expected_value.parse::<i128>().is_err() && expected_value.parse::<u128>().is_err() {
394                    return Err(
395                        syn::Error::new_spanned(
396                            &field.ty,
397                            format!("`if_then`: `expected_value`='{}' inválido para tipo {}", expected_value, target_ty_name)
398                        ).to_compile_error().into()
399                    );
400                }
401            } else if Self::is_float_ty(&target_ty_name) {
402                if expected_value.parse::<f64>().is_err() {
403                    return Err(
404                        syn::Error::new_spanned(
405                            &field.ty,
406                            format!("`if_then`: `expected_value`='{}' inválido para tipo {}", expected_value, target_ty_name)
407                        ).to_compile_error().into()
408                    );
409                }
410            } else if target_ty_name == "bool" {
411                if expected_value.parse::<bool>().is_err() {
412                    return Err(
413                        syn::Error::new_spanned(
414                            &field.ty,
415                            format!("`if_then`: `expected_value`='{}' inválido para tipo bool (use 'true' ou 'false')", expected_value)
416                        ).to_compile_error().into()
417                    );
418                }
419            } else {
420                return Err(
421                    syn::Error::new_spanned(
422                        &field.ty,
423                        format!("`if_then`: tipo do campo `{}` não suportado; use String ou numérico", target_ty_name)
424                    ).to_compile_error().into()
425                );
426            }
427        }
428
429        Ok(())
430    }
431
432
433    // ---------- Utilitários de extração ----------
434
435    fn expect_lit_str(expr: &syn::Expr, msg: &str) -> syn::Result<String> {
436        if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = expr {
437            Ok(s.value())
438        } else {
439            Err(syn::Error::new_spanned(expr, msg))
440        }
441    }
442
443    fn expect_lit_int(expr: &syn::Expr, msg: &str) -> syn::Result<usize> {
444        if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) = expr {
445            i.base10_parse::<usize>().map_err(|e| syn::Error::new_spanned(expr, e))
446        } else {
447            Err(syn::Error::new_spanned(expr, msg))
448        }
449    }
450
451    fn expect_lit_str_expr(expr: syn::Expr, msg: &str) -> syn::Result<String> {
452        if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = expr {
453            Ok(s.value())
454        } else {
455            Err(syn::Error::new_spanned(expr, msg))
456        }
457    }
458}
459
460
461// [FIX] helper para detectar Option<T>
462fn option_inner_type(ty: &Type) -> Option<&Type> {
463    if let Type::Path(tp) = ty {
464        if let Some(seg) = tp.path.segments.last() {
465            if seg.ident == "Option" {
466                if let PathArguments::AngleBracketed(args) = &seg.arguments {
467                    if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
468                        return Some(inner_ty);
469                    }
470                }
471            }
472        }
473    }
474    None
475}
476
477#[proc_macro_derive(ValidateCsv, attributes(validate))]
478pub fn validate_csv_derive(input: TokenStream) -> TokenStream {
479    let input = parse_macro_input!(input as DeriveInput);
480    let name = &input.ident;
481
482    let fields = match &input.data {
483        Data::Struct(data) => match &data.fields {
484            Fields::Named(f) => &f.named,
485            _ => {
486                return syn::Error::new_spanned(
487                    &data.fields,
488                    "only structs with named fields are supported",
489                )
490                .to_compile_error()
491                .into();
492            }
493        },
494        _ => {
495            return syn::Error::new_spanned(&input, "only structs are supported")
496                .to_compile_error()
497                .into();
498        }
499    };
500
501    let mut field_validations = Vec::new();
502
503    for field in fields {
504        let field_name = field.ident.as_ref().unwrap().clone();
505        let is_option = option_inner_type(&field.ty).is_some(); // [FIX] capturamos se é Option<T>
506        let mut validations = Vec::new();
507
508        for attr in &field.attrs {
509            if attr.path().is_ident("validate") {
510                match attr.parse_args_with(Validation::parse_validations) {
511                    Ok(mut v) => {
512                        // v.0.1.3: `required` só em Option<T>
513                        let has_required = v.iter().any(|vv| matches!(vv, Validation::Required)); // v.0.1.3
514                        if has_required && !is_option {                                          // v.0.1.3
515                            return syn::Error::new_spanned(
516                                &field.ty,
517                                format!("`required` can only be used on Option<T> fields (field `{}`)", field_name)
518                            ).to_compile_error().into();
519                        }
520
521                        // v.0.1.3: Restrições de tipo para validações baseadas em String
522                        let needs_string = v.iter().any(|vv| matches!(
523                            vv,
524                            Validation::Regex{..} | Validation::Length{..} | Validation::NotBlank
525                            | Validation::OneOf{..} | Validation::NotIn{..}
526                        )); // v.0.1.3
527                        if needs_string {
528                            let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); // v.0.1.3
529                            let ty_name = Validation::type_name_of(core_ty);                                  // v.0.1.3
530                            if ty_name != "String" {                                         // v.0.1.3
531                                return syn::Error::new_spanned(
532                                    core_ty,
533                                    format!("`regex`, `length`, `not_blank`, `one_of`, `not_in` require String (field `{}` is `{}`)", field_name, ty_name)
534                                ).to_compile_error().into();                                 // v.0.1.3
535                            }
536                        }
537
538                        // v.0.1.3: Restrições de tipo para `range`
539                        if let Some(is_float) = v.iter().find_map(|vv| {
540                            if let Validation::Range{is_float, ..} = vv { Some(*is_float) } else { None }
541                        }) { // v.0.1.3
542                            let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); // v.0.1.3
543                            let ty_name     = Validation::type_name_of(core_ty);
544                            let is_int      = Validation::is_int_ty(&ty_name);
545                            let is_float_ty = Validation::is_float_ty(&ty_name);
546                            if !(is_int || is_float_ty) {
547                                return syn::Error::new_spanned(
548                                    core_ty,
549                                    format!("`range` only applies to numeric fields (field `{}` is `{}`)", field_name, ty_name)
550                                ).to_compile_error().into(); // v.0.1.3
551                            }
552                            if is_float && !is_float_ty {
553                                return syn::Error::new_spanned(
554                                    core_ty,
555                                    format!("`range` with float literals requires float field (field `{}` is `{}`)", field_name, ty_name)
556                                ).to_compile_error().into(); // v.0.1.3
557                            }
558                            if !is_float && !is_int {
559                                return syn::Error::new_spanned(
560                                    core_ty,
561                                    format!("`range` with integer literals requires integer field (field `{}` is `{}`)", field_name, ty_name)
562                                ).to_compile_error().into(); // v.0.1.3
563                            }
564                        }
565
566                        // valida if_then para este campo (se houver)
567                        if let Err(ts) = Validation::validate_if_then_for_field(&v, field, &field_name, is_option, fields) {
568                            return ts; // já vem com to_compile_error().into()
569                        }
570
571                        validations.append(&mut v);
572                    },
573                    Err(e) => return e.to_compile_error().into(),
574                }
575            }
576        }
577
578        if !validations.is_empty() {
579            // v.0.1.3: calcular e guardar tipo efetivo para codegen (T se Option<T>, senão o próprio)
580            let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); // v.0.1.3
581            let core_ty_ts = quote! { #core_ty };                            // v.0.1.3
582
583            field_validations.push(FieldValidation {
584                field_name,
585                is_option,
586                validations,
587                core_ty_ts,   // v.0.1.3
588            });
589        }
590    }
591
592    let validation_arms = field_validations.into_iter().map(|fv| {
593        let field_name_str = fv.field_name.to_string();
594        let field_name_ident = fv.field_name;
595        let fv_is_option = fv.is_option;
596        let fv_core_ty_ts = fv.core_ty_ts.clone(); // v.0.1.3
597    
598        let checks = fv.validations.into_iter().map(|validation| {
599            match validation {
600                Validation::Required => {
601                    gen_required_check(&field_name_ident, &field_name_str)
602                }
603                Validation::NotBlank => {
604                    gen_not_blank_check(&field_name_ident, &field_name_str, fv_is_option)
605                }
606                Validation::Range { min, max, is_float: _ } => { // v.0.1.3
607                    gen_range_check(&field_name_ident, &field_name_str, fv_is_option, min, max, fv_core_ty_ts.clone()) // v.0.1.3
608                }
609                Validation::Length { min, max } => {
610                    gen_length_check(&field_name_ident, &field_name_str, fv_is_option, min, max)
611                }
612                Validation::Regex { regex } => {
613                    gen_regex_check(&field_name_ident, &field_name_str, fv_is_option, regex)
614                }
615                Validation::OneOf { values } => {
616                    gen_one_of_check(&field_name_ident, &field_name_str, fv_is_option, values)
617                }
618                Validation::NotIn { values } => {
619                    gen_not_in_check(&field_name_ident, &field_name_str, fv_is_option, values)
620                }
621                Validation::Custom { path } => {
622                    gen_custom_check(&field_name_ident, &field_name_str, fv_is_option, path)
623                }
624                Validation::IfThen { conditional_column, conditional_value, expected_value } => {
625                    // Gera o check if_then, tipando expected_value com o tipo efetivo do campo alvo (R)
626                    // e inferindo o tipo do campo condicional (U) via FromStr no runtime.
627                    gen_if_then(
628                        &field_name_ident,
629                        &field_name_str,
630                        fv_is_option,
631                        fv_core_ty_ts.clone(),
632                        conditional_column,
633                        conditional_value,
634                        expected_value,
635                    )
636                } // v 0.2.0
637            }
638        });
639    
640        quote! { #(#checks)* }
641    });
642
643    let expanded = quote! {
644        impl #name {
645            pub fn validate_csv(&self) -> ::core::result::Result<(), ::std::vec::Vec<::csv_schema_validator::ValidationError>> {
646                let mut errors = ::std::vec::Vec::new();
647                #(#validation_arms)*
648                if errors.is_empty() {
649                    Ok(())
650                } else {
651                    Err(errors)
652                }
653            }
654        }
655    };
656
657    TokenStream::from(expanded)
658}
659
660use proc_macro2::TokenStream as TokenStream2;
661
662// Helpers por regra
663fn gen_required_check(field_ident: &syn::Ident, field_name: &str) -> TokenStream2 {
664    quote! {
665        if (&self.#field_ident).is_none() {
666            errors.push(::csv_schema_validator::ValidationError {
667                field: #field_name.to_string(),
668                message: "mandatory field".to_string(),
669            });
670        }
671    }
672}
673
674fn gen_not_blank_check(field_ident: &syn::Ident, field_name: &str, is_option: bool) -> TokenStream2 {
675    if is_option {
676        quote! {
677            if let Some(value) = &self.#field_ident {
678                if value.trim().is_empty() {
679                    errors.push(::csv_schema_validator::ValidationError {
680                        field: #field_name.to_string(),
681                        message: "must not be blank or contain only whitespace".to_string(),
682                    });
683                }
684            }
685        }
686    } else {
687        quote! {
688            let value = &self.#field_ident;
689            if value.trim().is_empty() {
690                errors.push(::csv_schema_validator::ValidationError {
691                    field: #field_name.to_string(),
692                    message: "must not be blank or contain only whitespace".to_string(),
693                });
694            }
695        }
696    }
697}
698
699// v.0.1.3: `range` agora recebe min/max como Option<String> e usa o tipo efetivo do campo;
700// tipamos os literais com o tipo do campo, para o compilador checar compatibilidade/overflow.
701// Também produzimos mensagens específicas quando ambos os limites existem.
702fn gen_range_check(
703    field_ident: &syn::Ident,
704    field_name: &str,
705    is_option: bool,
706    min: Option<String>,         // v.0.1.3
707    max: Option<String>,         // v.0.1.3
708    core_ty_ts: proc_macro2::TokenStream, // v.0.1.3
709) -> TokenStream2 {
710    let min_ts = min.as_ref().map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).expect("invalid min literal")); // v.0.1.3
711    let max_ts = max.as_ref().map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).expect("invalid max literal")); // v.0.1.3
712
713    let min_bind = min_ts.as_ref().map(|ts| quote! { let __csv_min: #core_ty_ts = #ts; }); // v.0.1.3
714    let max_bind = max_ts.as_ref().map(|ts| quote! { let __csv_max: #core_ty_ts = #ts; }); // v.0.1.3
715
716    // v.0.1.3: mensagens amigáveis (normalizamos sufixo ".0")
717    fn normalize_for_msg(s: &str) -> String { // v.0.1.3
718        if let Some(stripped) = s.strip_suffix(".0") { stripped.to_string() } else { s.to_string() }
719    }
720    let msg_between = match (min.as_ref(), max.as_ref()) { // v.0.1.3
721        (Some(a), Some(b)) => format!("value out of expected range: {} to {}", normalize_for_msg(a), normalize_for_msg(b)),
722        _ => "value out of expected range".to_string(),
723    };
724    let msg_below = match min.as_ref() { // v.0.1.3
725        Some(a) => format!("value below min: {}", normalize_for_msg(a)),
726        None => "value below min".to_string(),
727    };
728    let msg_above = match max.as_ref() { // v.0.1.3
729        Some(b) => format!("value above max: {}", normalize_for_msg(b)),
730        None => "value above max".to_string(),
731    };
732
733    let cmp = match (min_bind.is_some(), max_bind.is_some()) {
734        (true, true) => quote! {
735            if !(__csv_min <= *value && *value <= __csv_max) {
736                errors.push(::csv_schema_validator::ValidationError {
737                    field: #field_name.to_string(),
738                    message: #msg_between.to_string(), // v.0.1.3
739                });
740            }
741        },
742        (true, false) => quote! {
743            if !(__csv_min <= *value) {
744                errors.push(::csv_schema_validator::ValidationError {
745                    field: #field_name.to_string(),
746                    message: #msg_below.to_string(), // v.0.1.3
747                });
748            }
749        },
750        (false, true) => quote! {
751            if !(*value <= __csv_max) {
752                errors.push(::csv_schema_validator::ValidationError {
753                    field: #field_name.to_string(),
754                    message: #msg_above.to_string(), // v.0.1.3
755                });
756            }
757        },
758        _ => quote! {}, // impossível pois já validamos no parse
759    };
760
761    if is_option {
762        quote! {
763            { #min_bind #max_bind
764              if let Some(value) = &self.#field_ident { #cmp } }
765        }
766    } else {
767        quote! {
768            { #min_bind #max_bind
769              let value = &self.#field_ident; #cmp }
770        }
771    }
772}
773
774fn gen_length_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, min: usize, max: usize) -> TokenStream2 {
775    if is_option {
776        quote! {
777            if let Some(value) = &self.#field_ident {
778                let len = value.len();
779                if len < #min || len > #max {
780                    errors.push(::csv_schema_validator::ValidationError {
781                        field: #field_name.to_string(),
782                        message: format!("length out of expected range: {} to {}", #min, #max),
783                    });
784                }
785            }
786        }
787    } else {
788        quote! {
789            let value = &self.#field_ident;
790            let len = value.len();
791            if len < #min || len > #max {
792                errors.push(::csv_schema_validator::ValidationError {
793                    field: #field_name.to_string(),
794                    message: format!("length out of expected range: {} to {}", #min, #max),
795                });
796            }
797        }
798    }
799}
800
801fn gen_regex_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, regex: String) -> TokenStream2 {
802    let body = quote! {
803        use ::csv_schema_validator::__private::once_cell::sync::Lazy;
804        use ::csv_schema_validator::__private::regex;
805        static RE: Lazy<Result<regex::Regex, regex::Error>> = Lazy::new(|| regex::Regex::new(#regex));
806
807        match RE.as_ref() {
808            Ok(compiled_regex) => {
809                if !compiled_regex.is_match(value) {
810                    errors.push(::csv_schema_validator::ValidationError {
811                        field: #field_name.to_string(),
812                        message: "does not match the expected pattern".to_string(),
813                    });
814                }
815            }
816            Err(e) => {
817                errors.push(::csv_schema_validator::ValidationError {
818                    field: #field_name.to_string(),
819                    message: format!("invalid regex '{}': {}", #regex, e),
820                });
821            }
822        }
823    };
824    if is_option {
825        quote! {
826            if let Some(value) = &self.#field_ident {
827                #body
828            }
829        }
830    } else {
831        quote! {
832            let value = &self.#field_ident;
833            #body
834        }
835    }
836}
837
838fn gen_one_of_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, values: Vec<String>) -> TokenStream2 {
839    let arr = values; // mantém o padrão já usado
840    if is_option {
841        quote! {
842            if let Some(value) = &self.#field_ident {
843                const __ALLOWED: &[&str] = &[#(#arr),*];
844                if !__ALLOWED.contains(&value.as_str()) {
845                    errors.push(::csv_schema_validator::ValidationError {
846                        field: #field_name.to_string(),
847                        message: format!("invalid value"),
848                    });
849                }
850            }
851        }
852    } else {
853        quote! {
854            let value = &self.#field_ident;
855            const __ALLOWED: &[&str] = &[#(#arr),*];
856            if !__ALLOWED.contains(&value.as_str()) {
857                errors.push(::csv_schema_validator::ValidationError {
858                    field: #field_name.to_string(),
859                    message: format!("invalid value"),
860                });
861            }
862        }
863    }
864}
865
866fn gen_not_in_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, values: Vec<String>) -> TokenStream2 {
867    let arr = values;
868    if is_option {
869        quote! {
870            if let Some(value) = &self.#field_ident {
871                const __FORBIDDEN: &[&str] = &[#(#arr),*];
872                if __FORBIDDEN.contains(&value.as_str()) {
873                    errors.push(::csv_schema_validator::ValidationError {
874                        field: #field_name.to_string(),
875                        message: format!("value not allowed"),
876                    });
877                }
878            }
879        }
880    } else {
881        quote! {
882            let value = &self.#field_ident;
883            const __FORBIDDEN: &[&str] = &[#(#arr),*];
884            if __FORBIDDEN.contains(&value.as_str()) {
885                errors.push(::csv_schema_validator::ValidationError {
886                    field: #field_name.to_string(),
887                    message: format!("value not allowed"),
888                });
889            }
890        }
891    }
892}
893
894fn gen_custom_check(field_ident: &syn::Ident, field_name: &str, is_option: bool, path: syn::Path) -> TokenStream2 {
895    if is_option {
896        quote! {
897            if let Some(value) = &self.#field_ident {
898                match #path(value) {
899                    Err(err) => {
900                        errors.push(::csv_schema_validator::ValidationError {
901                            field: #field_name.to_string(),
902                            message: format!("{}", err),
903                        });
904                    }
905                    Ok(()) => {}
906                }
907            }
908        }
909    } else {
910        quote! {
911            match #path(&self.#field_ident) {
912                Err(err) => {
913                    errors.push(::csv_schema_validator::ValidationError {
914                        field: #field_name.to_string(),
915                        message: format!("{}", err),
916                    });
917                }
918                Ok(()) => {}
919            }
920        }
921    }
922}
923
924
925// Gera a validação if_then:
926// Se self.<conditional_column> == conditional_value então
927//   self.<field_ident> deve existir (Some) e ser igual a expected_value.
928//
929// Observações importantes:
930// - Não assumimos tipos específicos. Em vez de tipar literais diretamente,
931//   usamos FromStr para converter as strings fornecidas no atributo para os
932//   tipos reais dos campos em tempo de execução:
933//     * expected_value -> convertido para o tipo efetivo do CAMPO ANOTADO (R)
934//       usando <R as FromStr>::from_str(...).
935//     * conditional_value -> convertido dinamicamente para o tipo do CAMPO
936//       CONDICIONAL (U) usando inferência com <_ as FromStr>::from_str(...)
937//       e comparação com *__cond_ref (o compilador infere U).
938// - Isso preserva compatibilidade com sinais, tamanhos e booleanos, sem
939//   "forçar" i32/f64. Para String, FromStr já devolve String.
940// - Se o parse falhar (embora já termos feito check em compile-time), geramos
941//   um erro amigável em runtime e ignoramos o restante do bloco.
942//
943fn gen_if_then(
944    target_field_ident: &syn::Ident,                 // campo anotado (alvo)
945    target_field_name: &str,                         // nome para mensagem
946    _target_is_option: bool,                         // já garantido ser Option<R>
947    target_core_ty_ts: proc_macro2::TokenStream,     // tipo efetivo R
948    conditional_column: String,                      // nome do campo condicional
949    conditional_value: String,                       // valor esperado no campo condicional (string literal)
950    expected_value: String,                          // valor que o campo alvo deve ter quando a condição é verdadeira
951) -> TokenStream2 {
952    let cond_ident = syn::Ident::new(&conditional_column, proc_macro2::Span::call_site());
953    let cond_val_str = conditional_value;
954    let expected_val_str = expected_value;
955
956    quote! {
957        {
958            // 1) Parse do expected_value para o tipo do campo alvo (R)
959            let __csv_expected_parse: ::core::result::Result<#target_core_ty_ts, _> =
960                <#target_core_ty_ts as ::core::str::FromStr>::from_str(#expected_val_str);
961
962            if let Ok(__csv_expected) = __csv_expected_parse {
963                // 2) Checagem da condição com helper genérico que monomorfiza em T (= tipo real do campo condicional)
964                //    Evita o uso de "<_ as FromStr>::from_str(...)" que causa E0283.
965                #[inline]
966                fn __csv_eq_parsed<T>(cond_ref: &T, s: &str) -> bool
967                where
968                    T: ::core::str::FromStr + ::core::cmp::PartialEq,
969                {
970                    match <T as ::core::str::FromStr>::from_str(s) {
971                        Ok(v) => *cond_ref == v,
972                        Err(_) => false,
973                    }
974                }
975
976                let __csv_condition_holds = match &self.#cond_ident {
977                    Some(__cond_ref) => __csv_eq_parsed(__cond_ref, #cond_val_str),
978                    None => false,
979                };
980
981                if __csv_condition_holds {
982                    match &self.#target_field_ident {
983                        Some(__v) if *__v == __csv_expected => { /* ok */ }
984                        Some(_) => {
985                            errors.push(::csv_schema_validator::ValidationError {
986                                field: #target_field_name.to_string(),
987                                message: format!(
988                                    "must be {} when {} == {}",
989                                    #expected_val_str, #conditional_column, #cond_val_str
990                                ),
991                            });
992                        }
993                        None => {
994                            errors.push(::csv_schema_validator::ValidationError {
995                                field: #target_field_name.to_string(),
996                                message: format!(
997                                    "must be {} when {} == {} (missing value)",
998                                    #expected_val_str, #conditional_column, #cond_val_str
999                                ),
1000                            });
1001                        }
1002                    }
1003                }
1004            } else {
1005                errors.push(::csv_schema_validator::ValidationError {
1006                    field: #target_field_name.to_string(),
1007                    message: format!(
1008                        "invalid expected_value '{}' for field type",
1009                        #expected_val_str
1010                    ),
1011                });
1012            }
1013        }
1014    }
1015}