From 28528cb772c426aebdcf55c02ab46aeb2fd771f2 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sat, 9 Mar 2024 23:06:29 +0100 Subject: [PATCH] WIP fix option --- CHANGELOG.md | 5 + Cargo.toml | 2 +- validator_derive/src/lib.rs | 64 +++++----- validator_derive/src/tokens/cards.rs | 7 +- validator_derive/src/tokens/contains.rs | 7 +- validator_derive/src/tokens/custom.rs | 11 +- .../src/tokens/does_not_contain.rs | 7 +- validator_derive/src/tokens/email.rs | 7 +- validator_derive/src/tokens/ip.rs | 11 +- validator_derive/src/tokens/length.rs | 7 +- validator_derive/src/tokens/must_match.rs | 7 +- .../src/tokens/non_control_character.rs | 7 +- validator_derive/src/tokens/range.rs | 7 +- validator_derive/src/tokens/regex.rs | 7 +- validator_derive/src/tokens/required.rs | 7 +- .../src/tokens/required_nested.rs | 9 +- validator_derive/src/tokens/url.rs | 11 +- validator_derive/src/types.rs | 117 ++++++++++++++++++ .../tests/run-pass/optional_field.rs | 24 ++++ 19 files changed, 235 insertions(+), 89 deletions(-) create mode 100644 validator_derive_tests/tests/run-pass/optional_field.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1a5401..36d365d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Changelog +## 0.17.1 (unreleased) + +- Fix regressions from the derive rewrite + + ## 0.17.0 (2024/03/04) - Derive macro has been entirely rewritten diff --git a/Cargo.toml b/Cargo.toml index 5e1c7b4f..35b694fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["validator", "validator_derive", "validator_derive_tests"] +members = ["validator", "validator_derive", "validator_derive_tests", "testing-bugs"] diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index 35ff2209..885665c8 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -32,157 +32,163 @@ impl ToTokens for ValidateField { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let field_name = self.ident.clone().unwrap(); let field_name_str = self.ident.clone().unwrap().to_string(); + let (actual_field, wrapper_closure) = self.if_let_option_wrapper(&field_name); // Length validation let length = if let Some(length) = self.length.clone() { - length_tokens(length, &field_name, &field_name_str) + wrapper_closure(length_tokens(length, &actual_field, &field_name_str)) } else { quote!() }; // Email validation let email = if let Some(email) = self.email.clone() { - email_tokens( + wrapper_closure(email_tokens( match email { Override::Inherit => Email::default(), Override::Explicit(e) => e, }, - &field_name, + &actual_field, &field_name_str, - ) + )) } else { quote!() }; // Credit card validation let card = if let Some(credit_card) = self.credit_card.clone() { - credit_card_tokens( + wrapper_closure(credit_card_tokens( match credit_card { Override::Inherit => Card::default(), Override::Explicit(c) => c, }, - &field_name, + &actual_field, &field_name_str, - ) + )) } else { quote!() }; // Url validation let url = if let Some(url) = self.url.clone() { - url_tokens( + wrapper_closure(url_tokens( match url { Override::Inherit => Url::default(), Override::Explicit(u) => u, }, - &field_name, + &actual_field, &field_name_str, - ) + )) } else { quote!() }; // Ip address validation let ip = if let Some(ip) = self.ip.clone() { - ip_tokens( + wrapper_closure(ip_tokens( match ip { Override::Inherit => Ip::default(), Override::Explicit(i) => i, }, - &field_name, + &actual_field, &field_name_str, - ) + )) } else { quote!() }; // Non control character validation let ncc = if let Some(ncc) = self.non_control_character.clone() { - non_control_char_tokens( + wrapper_closure(non_control_char_tokens( match ncc { Override::Inherit => NonControlCharacter::default(), Override::Explicit(n) => n, }, - &field_name, + &actual_field, &field_name_str, - ) + )) } else { quote!() }; // Range validation let range = if let Some(range) = self.range.clone() { - range_tokens(range, &field_name, &field_name_str) + wrapper_closure(range_tokens(range, &actual_field, &field_name_str)) } else { quote!() }; // Required validation let required = if let Some(required) = self.required.clone() { - required_tokens( + wrapper_closure(required_tokens( match required { Override::Inherit => Required::default(), Override::Explicit(r) => r, }, - &field_name, + &actual_field, &field_name_str, - ) + )) } else { quote!() }; // Required nested validation let required_nested = if let Some(required_nested) = self.required_nested.clone() { - required_nested_tokens( + wrapper_closure(required_nested_tokens( match required_nested { Override::Inherit => Required::default(), Override::Explicit(r) => r, }, - &field_name, + &actual_field, &field_name_str, - ) + )) } else { quote!() }; // Contains validation let contains = if let Some(contains) = self.contains.clone() { - contains_tokens(contains, &field_name, &field_name_str) + wrapper_closure(contains_tokens(contains, &actual_field, &field_name_str)) } else { quote!() }; // Does not contain validation let does_not_contain = if let Some(does_not_contain) = self.does_not_contain.clone() { - does_not_contain_tokens(does_not_contain, &field_name, &field_name_str) + wrapper_closure(does_not_contain_tokens( + does_not_contain, + &actual_field, + &field_name_str, + )) } else { quote!() }; // Must match validation let must_match = if let Some(must_match) = self.must_match.clone() { - must_match_tokens(must_match, &field_name, &field_name_str) + // TODO: handle option for other + wrapper_closure(must_match_tokens(must_match, &actual_field, &field_name_str)) } else { quote!() }; // Regex validation let regex = if let Some(regex) = self.regex.clone() { - regex_tokens(regex, &field_name, &field_name_str) + wrapper_closure(regex_tokens(regex, &actual_field, &field_name_str)) } else { quote!() }; // Custom validation let custom = if let Some(custom) = self.custom.clone() { - custom_tokens(custom, &field_name, &field_name_str) + wrapper_closure(custom_tokens(custom, &actual_field, &field_name_str)) } else { quote!() }; let nested = if let Some(n) = self.nested { if n { - nested_tokens(&field_name, &field_name_str) + wrapper_closure(nested_tokens(&field_name, &field_name_str)) } else { quote!() } diff --git a/validator_derive/src/tokens/cards.rs b/validator_derive/src/tokens/cards.rs index 6b9813c8..288efc93 100644 --- a/validator_derive/src/tokens/cards.rs +++ b/validator_derive/src/tokens/cards.rs @@ -1,22 +1,21 @@ use quote::quote; -use syn::Ident; use crate::types::Card; use crate::utils::{quote_code, quote_message}; pub fn credit_card_tokens( credit_card: Card, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let message = quote_message(credit_card.message); let code = quote_code(credit_card.code, "credit_card"); quote! { - if !self.#field_name.validate_credit_card() { + if !#field_name.validate_credit_card() { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/contains.rs b/validator_derive/src/tokens/contains.rs index f63e89e6..a03dc4dd 100644 --- a/validator_derive/src/tokens/contains.rs +++ b/validator_derive/src/tokens/contains.rs @@ -1,12 +1,11 @@ use quote::quote; -use syn::Ident; use crate::types::Contains; use crate::utils::{quote_code, quote_message}; pub fn contains_tokens( contains: Contains, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let p = contains.pattern; @@ -17,11 +16,11 @@ pub fn contains_tokens( let code = quote_code(contains.code, "contains"); quote! { - if !self.#field_name.validate_contains(#needle) { + if !#field_name.validate_contains(#needle) { #code #message #needle_err - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/custom.rs b/validator_derive/src/tokens/custom.rs index 83822718..fb434a52 100644 --- a/validator_derive/src/tokens/custom.rs +++ b/validator_derive/src/tokens/custom.rs @@ -1,24 +1,23 @@ use quote::quote; -use syn::Ident; use crate::types::Custom; use crate::utils::quote_message; pub fn custom_tokens( custom: Custom, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let fn_call = custom.function.unwrap(); let args = if let Some(arg) = custom.use_context { if arg { - quote!(&self.#field_name, args) + quote!(&#field_name, args) } else { - quote! (&self.#field_name) + quote! (&#field_name) } } else { - quote!(&self.#field_name) + quote!(&#field_name) }; let message = quote_message(custom.message); @@ -37,7 +36,7 @@ pub fn custom_tokens( ::std::result::Result::Err(mut err) => { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/does_not_contain.rs b/validator_derive/src/tokens/does_not_contain.rs index 79b81847..e96e6617 100644 --- a/validator_derive/src/tokens/does_not_contain.rs +++ b/validator_derive/src/tokens/does_not_contain.rs @@ -1,12 +1,11 @@ use quote::quote; -use syn::Ident; use crate::types::DoesNotContain; use crate::utils::{quote_code, quote_message}; pub fn does_not_contain_tokens( does_not_contain: DoesNotContain, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let p = does_not_contain.pattern; @@ -18,11 +17,11 @@ pub fn does_not_contain_tokens( let code = quote_code(does_not_contain.code, "does_not_contain"); quote! { - if !self.#field_name.validate_does_not_contain(#needle) { + if !#field_name.validate_does_not_contain(#needle) { #code #message #needle_err - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/email.rs b/validator_derive/src/tokens/email.rs index c42f4020..b44f88c6 100644 --- a/validator_derive/src/tokens/email.rs +++ b/validator_derive/src/tokens/email.rs @@ -1,22 +1,21 @@ use quote::quote; -use syn::Ident; use crate::types::Email; use crate::utils::{quote_code, quote_message}; pub fn email_tokens( email: Email, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let message = quote_message(email.message); let code = quote_code(email.code, "email"); quote! { - if !self.#field_name.validate_email() { + if !#field_name.validate_email() { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/ip.rs b/validator_derive/src/tokens/ip.rs index 3dbaf283..8c002427 100644 --- a/validator_derive/src/tokens/ip.rs +++ b/validator_derive/src/tokens/ip.rs @@ -1,10 +1,13 @@ use quote::quote; -use syn::Ident; use crate::types::Ip; use crate::utils::{quote_code, quote_message}; -pub fn ip_tokens(ip: Ip, field_name: &Ident, field_name_str: &str) -> proc_macro2::TokenStream { +pub fn ip_tokens( + ip: Ip, + field_name: &proc_macro2::TokenStream, + field_name_str: &str, +) -> proc_macro2::TokenStream { let message = quote_message(ip.message); let code = quote_code(ip.code, "ip"); @@ -32,10 +35,10 @@ pub fn ip_tokens(ip: Ip, field_name: &Ident, field_name_str: &str) -> proc_macro }; quote! { - if !self.#field_name.#version { + if !#field_name.#version { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/length.rs b/validator_derive/src/tokens/length.rs index d0065a08..16f78eee 100644 --- a/validator_derive/src/tokens/length.rs +++ b/validator_derive/src/tokens/length.rs @@ -1,12 +1,11 @@ use quote::quote; -use syn::Ident; use crate::types::Length; use crate::utils::{quote_code, quote_message}; pub fn length_tokens( length: Length, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let (min, min_err) = if let Some(v) = length.min.as_ref() { @@ -29,13 +28,13 @@ pub fn length_tokens( let code = quote_code(length.code, "length"); quote! { - if !self.#field_name.validate_length(#min, #max, #equal) { + if !#field_name.validate_length(#min, #max, #equal) { #code #message #min_err #max_err #equal_err - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/must_match.rs b/validator_derive/src/tokens/must_match.rs index 1819904d..63647f3b 100644 --- a/validator_derive/src/tokens/must_match.rs +++ b/validator_derive/src/tokens/must_match.rs @@ -1,12 +1,11 @@ use quote::quote; -use syn::Ident; use crate::types::MustMatch; use crate::utils::{quote_code, quote_message}; pub fn must_match_tokens( must_match: MustMatch, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let o = must_match.other; @@ -17,11 +16,11 @@ pub fn must_match_tokens( let code = quote_code(must_match.code, "must_match"); quote! { - if !::validator::validate_must_match(&self.#field_name, &#other) { + if !::validator::validate_must_match(&#field_name, &#other) { #code #message #other_err - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/non_control_character.rs b/validator_derive/src/tokens/non_control_character.rs index 4c0b9982..8f868ab2 100644 --- a/validator_derive/src/tokens/non_control_character.rs +++ b/validator_derive/src/tokens/non_control_character.rs @@ -1,22 +1,21 @@ use quote::quote; -use syn::Ident; use crate::types::NonControlCharacter; use crate::utils::{quote_code, quote_message}; pub fn non_control_char_tokens( non_control_char: NonControlCharacter, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let message = quote_message(non_control_char.message); let code = quote_code(non_control_char.code, "non_control_character"); quote! { - if !self.#field_name.validate_non_control_character() { + if !#field_name.validate_non_control_character() { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/range.rs b/validator_derive/src/tokens/range.rs index 7a0b0b4a..0c1d8f84 100644 --- a/validator_derive/src/tokens/range.rs +++ b/validator_derive/src/tokens/range.rs @@ -1,12 +1,11 @@ use quote::quote; -use syn::Ident; use crate::types::Range; use crate::utils::{quote_code, quote_message}; pub fn range_tokens( range: Range, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let (min, min_err) = if let Some(m) = range.min { @@ -37,14 +36,14 @@ pub fn range_tokens( let code = quote_code(range.code, "range"); quote! { - if !self.#field_name.validate_range(#min, #max, #ex_min, #ex_max) { + if !#field_name.validate_range(#min, #max, #ex_min, #ex_max) { #code #message #min_err #max_err #ex_min_err #ex_max_err - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/regex.rs b/validator_derive/src/tokens/regex.rs index fe3d1ca3..cb97331e 100644 --- a/validator_derive/src/tokens/regex.rs +++ b/validator_derive/src/tokens/regex.rs @@ -1,12 +1,11 @@ use quote::quote; -use syn::Ident; use crate::types::Regex; use crate::utils::{quote_code, quote_message}; pub fn regex_tokens( regex: Regex, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let path = regex.path; @@ -14,10 +13,10 @@ pub fn regex_tokens( let code = quote_code(regex.code, "regex"); quote! { - if !&self.#field_name.validate_regex(&#path) { + if !&#field_name.validate_regex(&#path) { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/required.rs b/validator_derive/src/tokens/required.rs index 22926e02..35948ba4 100644 --- a/validator_derive/src/tokens/required.rs +++ b/validator_derive/src/tokens/required.rs @@ -1,22 +1,21 @@ use quote::quote; -use syn::Ident; use crate::types::Required; use crate::utils::{quote_code, quote_message}; pub fn required_tokens( required: Required, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let message = quote_message(required.message); let code = quote_code(required.code, "required"); quote! { - if !self.#field_name.validate_required() { + if !#field_name.validate_required() { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/tokens/required_nested.rs b/validator_derive/src/tokens/required_nested.rs index 8d57c78e..2eaa1443 100644 --- a/validator_derive/src/tokens/required_nested.rs +++ b/validator_derive/src/tokens/required_nested.rs @@ -1,22 +1,21 @@ use quote::quote; -use syn::Ident; use crate::types::Required; use crate::utils::{quote_code, quote_message}; pub fn required_nested_tokens( required: Required, - field_name: &Ident, + field_name: &proc_macro2::TokenStream, field_name_str: &str, ) -> proc_macro2::TokenStream { let message = quote_message(required.message); let code = quote_code(required.code, "required"); - + // TODO: fix me quote! { - if !self.#field_name.validate_required() { + if !#field_name.validate_required() { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } diff --git a/validator_derive/src/tokens/url.rs b/validator_derive/src/tokens/url.rs index 4c5d9d6e..af38adac 100644 --- a/validator_derive/src/tokens/url.rs +++ b/validator_derive/src/tokens/url.rs @@ -1,18 +1,21 @@ use quote::quote; -use syn::Ident; use crate::types::Url; use crate::utils::{quote_code, quote_message}; -pub fn url_tokens(url: Url, field_name: &Ident, field_name_str: &str) -> proc_macro2::TokenStream { +pub fn url_tokens( + url: Url, + field_name: &proc_macro2::TokenStream, + field_name_str: &str, +) -> proc_macro2::TokenStream { let message = quote_message(url.message); let code = quote_code(url.code, "url"); quote! { - if !self.#field_name.validate_url() { + if !#field_name.validate_url() { #code #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); errors.add(#field_name_str, err); } } diff --git a/validator_derive/src/types.rs b/validator_derive/src/types.rs index a1b24f94..a925a5fe 100644 --- a/validator_derive/src/types.rs +++ b/validator_derive/src/types.rs @@ -2,17 +2,65 @@ use darling::util::Override; use darling::{FromField, FromMeta}; use proc_macro_error::abort; +use quote::quote; use syn::spanned::Spanned; use syn::{Expr, Field, Ident, Path}; use crate::utils::get_attr; +static OPTIONS_TYPE: [&str; 3] = ["Option|", "std|option|Option|", "core|option|Option|"]; +// static NUMBER_TYPES: [&str; 42] = [ +// "usize", +// "u8", +// "u16", +// "u32", +// "u64", +// "u128", +// "isize", +// "i8", +// "i16", +// "i32", +// "i64", +// "i128", +// "f32", +// "f64", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// "Option>", +// ]; + // This struct holds all the validation information on a field // The "ident" and "ty" fields are populated by `darling` // The others are our attributes for example: // #[validate(email(message = "asdfg"))] // ^^^^^ // + #[derive(Debug, FromField, Clone)] #[darling(attributes(validate))] pub struct ValidateField { @@ -104,6 +152,75 @@ impl ValidateField { } } } + + /// How many Option u8 { + fn find_option(mut count: u8, ty: &syn::Type) -> u8 { + if let syn::Type::Path(p) = ty { + let idents_of_path = + p.path.segments.iter().into_iter().fold(String::new(), |mut acc, v| { + acc.push_str(&v.ident.to_string()); + acc.push('|'); + acc + }); + + if OPTIONS_TYPE.contains(&idents_of_path.as_str()) { + count += 1; + if let Some(p) = p.path.segments.first() { + if let syn::PathArguments::AngleBracketed(ref params) = p.arguments { + if let syn::GenericArgument::Type(ref ty) = params.args.first().unwrap() + { + count = find_option(count, ty); + } + } + } + } + } + count + } + + find_option(0, &self.ty) + } + + pub fn if_let_option_wrapper( + &self, + field_name: &Ident, + ) -> (proc_macro2::TokenStream, Box proc_macro2::TokenStream>) + { + let number_options = self.number_options(); + let field_name = field_name.clone(); + let actual_field = + if number_options > 0 { quote!(#field_name) } else { quote!(self.#field_name) }; + let option_val = quote!(ref #field_name); + + match number_options { + 0 => (actual_field.clone(), Box::new(move |tokens| tokens)), + 1 => ( + actual_field.clone(), + Box::new(move |tokens| { + quote!( + if let Some(#option_val) = self.#field_name { + #tokens + } + ) + }), + ), + 2 => ( + actual_field.clone(), + Box::new(move |tokens| { + quote!( + if let Some(Some(#option_val)) = self.#field_name { + #tokens + } + ) + }), + ), + _ => abort!( + field_name.span(), + "Validation on values nested in more than 2 Option are not supported" + ), + } + } } // Structs to hold the validation information and to provide attributes diff --git a/validator_derive_tests/tests/run-pass/optional_field.rs b/validator_derive_tests/tests/run-pass/optional_field.rs new file mode 100644 index 00000000..cb5fde12 --- /dev/null +++ b/validator_derive_tests/tests/run-pass/optional_field.rs @@ -0,0 +1,24 @@ +use validator::{Validate, ValidationError}; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct CountryCode(pub String); + +impl CountryCode { + pub fn validate(&self) -> Result<(), ValidationError> { + if true { + Ok(()) + } else { + Err(ValidationError::new("not a valid ISO-3166-1 alpha-2 country code")) + } + } +} + +#[derive(Debug, Validate)] +pub struct Foo { + #[validate(custom(function = "CountryCode::validate"))] + pub country: Option, + pub country2: Option>, + #[validate(range(min = 5, max = 10))] + pub age: Option, +} +fn main() {}