1extern 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
11struct FieldValidation {
13 field_name: Ident,
14 is_option: bool, validations: Vec<Validation>,
16 core_ty_ts: proc_macro2::TokenStream, }
18
19enum Validation {
21 Range { min: Option<String>, max: Option<String>, is_float: bool }, 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 }, }
31
32impl Validation {
33 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 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 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 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 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
170 *slot_val = Some(i.to_string());
171 *slot_is_float = false;
172 }
173 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
175 *slot_val = Some(f.to_string());
176 *slot_is_float = true;
177 }
178 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 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 if let (Some(ref a), Some(ref b)) = (&min, &max) {
223 if min_is_float {
224 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 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 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 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 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 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 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 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" {
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 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
461fn 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(); 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 let has_required = v.iter().any(|vv| matches!(vv, Validation::Required)); if has_required && !is_option { 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 let needs_string = v.iter().any(|vv| matches!(
523 vv,
524 Validation::Regex{..} | Validation::Length{..} | Validation::NotBlank
525 | Validation::OneOf{..} | Validation::NotIn{..}
526 )); if needs_string {
528 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(
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(); }
536 }
537
538 if let Some(is_float) = v.iter().find_map(|vv| {
540 if let Validation::Range{is_float, ..} = vv { Some(*is_float) } else { None }
541 }) { let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); 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(); }
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(); }
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(); }
564 }
565
566 if let Err(ts) = Validation::validate_if_then_for_field(&v, field, &field_name, is_option, fields) {
568 return ts; }
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 let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); let core_ty_ts = quote! { #core_ty }; field_validations.push(FieldValidation {
584 field_name,
585 is_option,
586 validations,
587 core_ty_ts, });
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(); 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: _ } => { gen_range_check(&field_name_ident, &field_name_str, fv_is_option, min, max, fv_core_ty_ts.clone()) }
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 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 } }
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
662fn 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
699fn gen_range_check(
703 field_ident: &syn::Ident,
704 field_name: &str,
705 is_option: bool,
706 min: Option<String>, max: Option<String>, core_ty_ts: proc_macro2::TokenStream, ) -> TokenStream2 {
710 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() }
719 }
720 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)),
722 _ => "value out of expected range".to_string(),
723 };
724 let msg_below = match min.as_ref() { 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() { 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(), });
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(), });
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(), });
756 }
757 },
758 _ => quote! {}, };
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; 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
925fn gen_if_then(
944 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 {
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 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 #[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 => { }
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}