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}
30
31impl Validation {
32 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 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 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 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
143 *slot_val = Some(i.to_string());
144 *slot_is_float = false;
145 }
146 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Float(f), .. }) => {
148 *slot_val = Some(f.to_string());
149 *slot_is_float = true;
150 }
151 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 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 if let (Some(ref a), Some(ref b)) = (&min, &max) {
196 if min_is_float {
197 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 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 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
277fn 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(); 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 let has_required = v.iter().any(|vv| matches!(vv, Validation::Required)); if has_required && !is_option { 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 let needs_string = v.iter().any(|vv| matches!(
339 vv,
340 Validation::Regex{..} | Validation::Length{..} | Validation::NotBlank
341 | Validation::OneOf{..} | Validation::NotIn{..}
342 )); if needs_string {
344 let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); let ty_name = if let Type::Path(tp) = core_ty { tp.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default()
347 } else { String::new() }; if ty_name != "String" { 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(); }
354 }
355
356 if let Some(is_float) = v.iter().find_map(|vv| {
358 if let Validation::Range{is_float, ..} = vv { Some(*is_float) } else { None }
359 }) { let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); let ty_name = if let Type::Path(tp) = core_ty { tp.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default()
363 } else { String::new() }; let is_int = matches!(ty_name.as_str(),
365 "i8"|"i16"|"i32"|"i64"|"i128"|"isize"|
366 "u8"|"u16"|"u32"|"u64"|"u128"|"usize"
367 ); let is_float_ty = matches!(ty_name.as_str(), "f32"|"f64"); 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(); }
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(); }
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(); }
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 let core_ty = option_inner_type(&field.ty).unwrap_or(&field.ty); let core_ty_ts = quote! { #core_ty }; field_validations.push(FieldValidation {
402 field_name,
403 is_option,
404 validations,
405 core_ty_ts, });
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(); 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: _ } => { gen_range_check(&field_name_ident, &field_name_str, fv_is_option, min, max, fv_core_ty_ts.clone()) }
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
467fn 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
504fn gen_range_check(
508 field_ident: &syn::Ident,
509 field_name: &str,
510 is_option: bool,
511 min: Option<String>, max: Option<String>, core_ty_ts: proc_macro2::TokenStream, ) -> TokenStream2 {
515 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() }
524 }
525 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)),
527 _ => "value out of expected range".to_string(),
528 };
529 let msg_below = match min.as_ref() { 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() { 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(), });
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(), });
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(), });
561 }
562 },
563 _ => quote! {}, };
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; 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}