From 283a00aebc5e1f6bbbc69c6bb611e216060cc803 Mon Sep 17 00:00:00 2001 From: pintariching <64165058+pintariching@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:27:58 +0200 Subject: [PATCH] Rewrite derive macro implementation (#262) * Rewrite derive macro implementation * Add better error handling * Add new types to validator macro * Add empty files in tokens module * Removed i32 tests for length * Fix email to pass tests * fix length validation trait to work with u64 only * Add credit card token generation to macro * Add url token generation to macro * Add ValidateIp trait * Add ip token generation to macro * Remove unneeded import * Export ValidateIp trait from main crate * Add tests for ValidateIp macro * Add non control character token generation to macro * Add test for range with enums * Add range token generation to macro * Add required token generation to macro * Fix ValidationErrors merge function * Move contains trait to contains.rs * Add ValidateContains trait and fix tests * Add ValidateContains and DoesNotContain traits to macro * Add must match token generation to macro * Add regex token generation * Add custom validation token generation to macro * Add custom validation with arguments to macro * Add custom validation with args with contexts * Add custom validation recommit * Fix custom validation to work without lifetimes * Change custom validation to use closures * Fix tests for custom validation * Change custom validation to implement FnOnce and add tests * Remove code used for experementing * Remove unneeded code * Remove unused imports * Add schema with arguments and fix tests * Impl ValidateLength trait for Option types * Fix impls for ValidateRange * Fix duplicate use statements * Fix tests and add Option impls for Contains * Implement ValidateUrl traits for specific types * Implement ValidateEmail traits for specific types * Fix some tests and warnings * Add regex trait for validation * Fix to pass tests in validator lib * Fix range validation trait to pass tests * Update and remove unneeded dependencies * Add ValidateNested trait * Fix custom and nested validation * Fix Regex validator to work with OnceCell and OnceLock * Fix derive macro to support all the arguments * Remove unneeded functions and fix tests * Remove validator_types crate and fix tests * Update CI workflow * Fix custom to be used in nested validations * Fix custom validation to use context * Add custom nested validation with args test * Fix validation to use Validation trait * Remove conflicting trait * Fixed tests and remove ValidateContext trait * Fix nested validation * Fix custom args test * Add `nest_all_fields` attribute * Add better error handling and start fixing UI tests * Pass almost all tests * Add skip_on_field_errors attribute to schema * Remove rust beta channel from workflow * Use proc_macro_error instead of darling diagnostics feature * Conditionally run UI tests * Fix ci.yml * Fix ci.yml * Remove UI test for wrong type on range * Change trait impls to be consistent * Run cargo clippy --fix * Replace if else with match * Remove cargo-expand leftovers * Replace underscore with `#[allow(unused)]` * Add support for multiple schema validations * Remove serde original field name test --- .github/workflows/ci.yml | 49 +- Cargo.toml | 8 +- rustfmt.toml | 2 +- validator/Cargo.toml | 11 +- validator/src/lib.rs | 27 +- validator/src/traits.rs | 59 +- validator/src/types.rs | 18 +- validator/src/validation/cards.rs | 28 +- validator/src/validation/contains.rs | 208 +++- validator/src/validation/does_not_contain.rs | 37 +- validator/src/validation/email.rs | 123 ++- validator/src/validation/ip.rs | 73 +- validator/src/validation/length.rs | 497 ++++++++-- validator/src/validation/mod.rs | 2 + validator/src/validation/nested.rs | 195 ++++ .../src/validation/non_control_character.rs | 27 +- validator/src/validation/range.rs | 209 +++- validator/src/validation/regex.rs | 175 ++++ validator/src/validation/required.rs | 5 - validator/src/validation/urls.rs | 101 +- validator_derive/Cargo.toml | 15 +- validator_derive/src/asserts.rs | 168 ---- validator_derive/src/lib.rs | 926 +++++++----------- validator_derive/src/lit.rs | 80 -- validator_derive/src/quoting.rs | 619 ------------ validator_derive/src/tokens/cards.rs | 23 + validator_derive/src/tokens/contains.rs | 28 + validator_derive/src/tokens/custom.rs | 45 + .../src/tokens/does_not_contain.rs | 29 + validator_derive/src/tokens/email.rs | 23 + validator_derive/src/tokens/ip.rs | 42 + validator_derive/src/tokens/length.rs | 42 + validator_derive/src/tokens/mod.rs | 16 + validator_derive/src/tokens/must_match.rs | 28 + validator_derive/src/tokens/nested.rs | 10 + .../src/tokens/non_control_character.rs | 23 + validator_derive/src/tokens/range.rs | 51 + validator_derive/src/tokens/regex.rs | 24 + validator_derive/src/tokens/required.rs | 23 + .../src/tokens/required_nested.rs | 27 + validator_derive/src/tokens/schema.rs | 52 + validator_derive/src/tokens/url.rs | 19 + validator_derive/src/types.rs | 214 ++++ validator_derive/src/utils.rs | 140 +++ validator_derive/src/validation.rs | 466 --------- validator_derive_tests/Cargo.toml | 15 +- validator_derive_tests/src/main.rs | 3 - .../compile-fail/custom/custom_not_string.rs | 2 +- .../custom/custom_not_string.stderr | 12 +- .../defined_args_in_custom.rs} | 2 +- .../custom/defined_args_in_custom.stderr | 5 + .../compile-fail/custom/different_lifetime.rs | 9 +- .../custom/different_lifetime.stderr | 12 +- .../custom/function_without_reference.rs | 9 - .../custom/function_without_reference.stderr | 5 - .../custom/missing_function_arg.rs | 2 +- .../custom/missing_function_arg.stderr | 8 +- .../custom/multiple_types_in_arg.rs | 9 - .../custom/multiple_types_in_arg.stderr | 6 - .../custom/type_without_reference.rs | 9 - .../custom/type_without_reference.stderr | 5 - .../custom/validate_not_impl_with_args.rs | 16 - .../custom/validate_not_impl_with_args.stderr | 12 - .../length/equal_and_min_max_set.stderr | 10 +- .../tests/compile-fail/length/no_args.rs | 1 + .../tests/compile-fail/length/no_args.stderr | 12 +- .../compile-fail/length/unknown_arg.stderr | 4 +- .../tests/compile-fail/length/wrong_type.rs | 2 +- .../compile-fail/length/wrong_type.stderr | 10 +- .../must_match/field_doesnt_exist.rs | 2 +- .../must_match/field_doesnt_exist.stderr | 12 +- .../must_match/field_type_doesnt_match.rs | 2 +- .../must_match/field_type_doesnt_match.stderr | 20 +- .../must_match/unexpected_name_value.rs | 2 +- .../must_match/unexpected_name_value.stderr | 6 +- .../compile-fail/no_nested_validations.rs | 2 +- .../compile-fail/no_nested_validations.stderr | 8 +- .../tests/compile-fail/no_validations.rs | 9 - .../tests/compile-fail/no_validations.stderr | 5 - .../tests/compile-fail/not_a_struct.stderr | 10 +- .../tests/compile-fail/range/no_args.stderr | 10 +- .../compile-fail/range/unknown_arg.stderr | 4 +- .../compile-fail/range/wrong_type.stderr | 5 - .../schema/missing_function.stderr | 4 +- .../unexpected_list_validator.stderr | 4 +- .../compile-fail/unexpected_validator.stderr | 4 +- .../tests/compile-fail/unnamed_fields.stderr | 13 +- validator_derive_tests/tests/compile_test.rs | 1 + validator_derive_tests/tests/complex.rs | 86 +- validator_derive_tests/tests/contains.rs | 4 +- validator_derive_tests/tests/credit_card.rs | 4 +- validator_derive_tests/tests/custom.rs | 57 +- validator_derive_tests/tests/custom_args.rs | 208 ++-- .../tests/does_not_contain.rs | 6 +- validator_derive_tests/tests/email.rs | 6 +- validator_derive_tests/tests/ip.rs | 181 ++++ validator_derive_tests/tests/length.rs | 56 +- validator_derive_tests/tests/must_match.rs | 4 +- .../tests/nest_all_fields.rs | 58 ++ validator_derive_tests/tests/nested.rs | 473 ++++++--- validator_derive_tests/tests/non_control.rs | 4 +- validator_derive_tests/tests/range.rs | 28 + validator_derive_tests/tests/regex.rs | 63 +- .../tests/run-pass/absolute_path.rs | 12 +- .../tests/run-pass/custom.rs | 4 +- .../tests/run-pass/must_match.rs | 4 +- .../tests/run-pass/range.rs | 24 +- .../tests/run-pass/regex.rs | 4 +- validator_derive_tests/tests/schema.rs | 44 +- validator_derive_tests/tests/schema_args.rs | 64 +- validator_derive_tests/tests/skip.rs | 40 + .../tests/unsupported_array.rs | 4 +- validator_derive_tests/tests/url.rs | 8 +- validator_types/Cargo.toml | 20 - validator_types/src/lib.rs | 114 --- 115 files changed, 3829 insertions(+), 3041 deletions(-) create mode 100644 validator/src/validation/nested.rs create mode 100644 validator/src/validation/regex.rs delete mode 100644 validator_derive/src/asserts.rs delete mode 100644 validator_derive/src/lit.rs delete mode 100644 validator_derive/src/quoting.rs create mode 100644 validator_derive/src/tokens/cards.rs create mode 100644 validator_derive/src/tokens/contains.rs create mode 100644 validator_derive/src/tokens/custom.rs create mode 100644 validator_derive/src/tokens/does_not_contain.rs create mode 100644 validator_derive/src/tokens/email.rs create mode 100644 validator_derive/src/tokens/ip.rs create mode 100644 validator_derive/src/tokens/length.rs create mode 100644 validator_derive/src/tokens/mod.rs create mode 100644 validator_derive/src/tokens/must_match.rs create mode 100644 validator_derive/src/tokens/nested.rs create mode 100644 validator_derive/src/tokens/non_control_character.rs create mode 100644 validator_derive/src/tokens/range.rs create mode 100644 validator_derive/src/tokens/regex.rs create mode 100644 validator_derive/src/tokens/required.rs create mode 100644 validator_derive/src/tokens/required_nested.rs create mode 100644 validator_derive/src/tokens/schema.rs create mode 100644 validator_derive/src/tokens/url.rs create mode 100644 validator_derive/src/types.rs create mode 100644 validator_derive/src/utils.rs delete mode 100644 validator_derive/src/validation.rs delete mode 100644 validator_derive_tests/src/main.rs rename validator_derive_tests/tests/compile-fail/{range/wrong_type.rs => custom/defined_args_in_custom.rs} (56%) create mode 100644 validator_derive_tests/tests/compile-fail/custom/defined_args_in_custom.stderr delete mode 100644 validator_derive_tests/tests/compile-fail/custom/function_without_reference.rs delete mode 100644 validator_derive_tests/tests/compile-fail/custom/function_without_reference.stderr delete mode 100644 validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.rs delete mode 100644 validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.stderr delete mode 100644 validator_derive_tests/tests/compile-fail/custom/type_without_reference.rs delete mode 100644 validator_derive_tests/tests/compile-fail/custom/type_without_reference.stderr delete mode 100644 validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.rs delete mode 100644 validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr delete mode 100644 validator_derive_tests/tests/compile-fail/no_validations.rs delete mode 100644 validator_derive_tests/tests/compile-fail/no_validations.stderr delete mode 100644 validator_derive_tests/tests/compile-fail/range/wrong_type.stderr create mode 100644 validator_derive_tests/tests/ip.rs create mode 100644 validator_derive_tests/tests/nest_all_fields.rs create mode 100644 validator_derive_tests/tests/skip.rs delete mode 100644 validator_types/Cargo.toml delete mode 100644 validator_types/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85c0421a..e27c4e66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,43 +3,28 @@ on: [push, pull_request] jobs: test_validator: - name: test validator - runs-on: ${{ matrix.os }} + name: Continuous integration + runs-on: ubuntu-latest strategy: matrix: - build: [pinned, stable, nightly] include: - - build: pinned - os: ubuntu-20.04 - rust: 1.56.1 - - build: stable - os: ubuntu-20.04 - rust: stable - - build: nightly - os: ubuntu-20.04 - rust: nightly + - rust: 1.70.0 # MSRV + - rust: stable + - rust: beta + - rust: nightly steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: dtolnay/rust-toolchain@stable with: - rust-version: ${{ matrix.rust }} + toolchain: ${{ matrix.rust }} + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 - name: Build System Info run: rustc --version - - - name: tests validator with all features - run: cd validator && cargo test --all-features - - name: tests validator with no features - run: cd validator && cargo test --no-default-features - - test_validator_derive: - name: test validator_derive - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v1 - - name: Install Rust - uses: hecrj/setup-rust-action@v1 - with: - rust-version: stable - - name: tests validator_derive with all features - run: cd validator_derive_tests && cargo test + - name: Tests + run: | + cargo build --no-default-features + cargo test --no-default-features + cargo build ${{ matrix.rust == 'stable' && '--all-features' || '--no-default-features --features card,unic,derive' }} + cargo test ${{ matrix.rust == 'stable' && '--all-features' || '--no-default-features --features card,unic,derive' }} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index be8fdda9..5e1c7b4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,3 @@ [workspace] -members = [ - "validator", - "validator_derive", - "validator_types", - "validator_derive_tests" -] +resolver = "2" +members = ["validator", "validator_derive", "validator_derive_tests"] diff --git a/rustfmt.toml b/rustfmt.toml index d45dc0e8..2a35f023 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -use_small_heuristics = "max" +use_small_heuristics = "Max" diff --git a/validator/Cargo.toml b/validator/Cargo.toml index d44fa1f5..08cb49cd 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -7,7 +7,7 @@ description = "Common validation functions (email, url, length, ...) and trait - homepage = "https://github.com/Keats/validator" repository = "https://github.com/Keats/validator" keywords = ["validation", "api", "validator"] -edition = "2018" +edition = "2021" readme = "../README.md" [dependencies] @@ -19,12 +19,11 @@ serde = "1" serde_derive = "1" serde_json = "1" validator_derive = { version = "0.16", path = "../validator_derive", optional = true } -card-validate = { version = "2.2", optional = true } +card-validate = { version = "2.3", optional = true } unic-ucd-common = { version = "0.9", optional = true } -indexmap = {version = "1", features = ["serde-1"], optional = true } - +indexmap = { version = "2.0.0", features = ["serde"], optional = true } [features] -card = ["card-validate", "validator_derive/card"] -unic = ["unic-ucd-common", "validator_derive/unic"] +card = ["card-validate"] +unic = ["unic-ucd-common"] derive = ["validator_derive"] diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 863a3478..da38576b 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -67,23 +67,22 @@ mod types; mod validation; #[cfg(feature = "card")] -pub use validation::cards::{validate_credit_card, ValidateCreditCard}; -pub use validation::contains::validate_contains; -pub use validation::does_not_contain::validate_does_not_contain; -pub use validation::email::{validate_email, ValidateEmail}; -pub use validation::ip::{validate_ip, validate_ip_v4, validate_ip_v6}; -pub use validation::length::{validate_length, ValidateLength}; +pub use validation::cards::ValidateCreditCard; +pub use validation::contains::ValidateContains; +pub use validation::does_not_contain::ValidateDoesNotContain; +pub use validation::email::ValidateEmail; +pub use validation::ip::ValidateIp; +pub use validation::length::ValidateLength; pub use validation::must_match::validate_must_match; +pub use validation::nested::ValidateNested; #[cfg(feature = "unic")] -pub use validation::non_control_character::{ - validate_non_control_character, ValidateNonControlCharacter, -}; -pub use validation::range::{validate_range, ValidateRange}; +pub use validation::non_control_character::ValidateNonControlCharacter; +pub use validation::range::ValidateRange; +pub use validation::regex::ValidateRegex; +pub use validation::required::ValidateRequired; +pub use validation::urls::ValidateUrl; -pub use validation::required::{validate_required, ValidateRequired}; -pub use validation::urls::{validate_url, ValidateUrl}; - -pub use traits::{Contains, HasLen, Validate, ValidateArgs}; +pub use traits::{HasLen, Validate, ValidateArgs}; pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind}; #[cfg(feature = "derive")] diff --git a/validator/src/traits.rs b/validator/src/traits.rs index 05378bc6..f28b4848 100644 --- a/validator/src/traits.rs +++ b/validator/src/traits.rs @@ -144,49 +144,6 @@ impl HasLen for IndexSet { } } -/// Trait to implement if one wants to make the `contains` validator -/// work for more types -pub trait Contains { - #[must_use] - fn has_element(&self, needle: &str) -> bool; -} - -impl Contains for String { - fn has_element(&self, needle: &str) -> bool { - self.contains(needle) - } -} - -impl<'a> Contains for &'a String { - fn has_element(&self, needle: &str) -> bool { - self.contains(needle) - } -} - -impl<'a> Contains for &'a str { - fn has_element(&self, needle: &str) -> bool { - self.contains(needle) - } -} - -impl<'a> Contains for Cow<'a, str> { - fn has_element(&self, needle: &str) -> bool { - self.contains(needle) - } -} - -impl Contains for HashMap { - fn has_element(&self, needle: &str) -> bool { - self.contains_key(needle) - } -} - -impl<'a, S, H: ::std::hash::BuildHasher> Contains for &'a HashMap { - fn has_element(&self, needle: &str) -> bool { - self.contains_key(needle) - } -} - /// This is the original trait that was implemented by deriving `Validate`. It will still be /// implemented for struct validations that don't take custom arguments. The call is being /// forwarded to the `ValidateArgs<'v_a>` trait. @@ -207,6 +164,20 @@ impl Validate for &T { /// The `Args` type can use the lifetime `'v_a` to pass references onto the validator. pub trait ValidateArgs<'v_a> { type Args; + fn validate_with_args(&self, args: Self::Args) -> Result<(), ValidationErrors>; +} + +impl<'v_a, T, U> ValidateArgs<'v_a> for Option +where + T: ValidateArgs<'v_a, Args = U>, +{ + type Args = U; - fn validate_args(&self, args: Self::Args) -> Result<(), ValidationErrors>; + fn validate_with_args(&self, args: Self::Args) -> Result<(), ValidationErrors> { + if let Some(nested) = self { + T::validate_with_args(nested, args) + } else { + Ok(()) + } + } } diff --git a/validator/src/types.rs b/validator/src/types.rs index e8d1de2f..3bbbf1f3 100644 --- a/validator/src/types.rs +++ b/validator/src/types.rs @@ -40,7 +40,7 @@ pub enum ValidationErrorsKind { } #[derive(Default, Debug, Serialize, Clone, PartialEq)] -pub struct ValidationErrors(HashMap<&'static str, ValidationErrorsKind>); +pub struct ValidationErrors(pub HashMap<&'static str, ValidationErrorsKind>); impl ValidationErrors { pub fn new() -> ValidationErrors { @@ -58,6 +58,22 @@ impl ValidationErrors { } } + pub fn merge_self( + &mut self, + field: &'static str, + child: Result<(), ValidationErrors>, + ) -> &mut ValidationErrors { + match child { + Ok(()) => self, + Err(errors) => { + for (_, e) in errors.0 { + self.add_nested(field, e); + } + self + } + } + } + /// Returns the combined outcome of a struct's validation result along with the nested /// validation result for one of its fields. pub fn merge( diff --git a/validator/src/validation/cards.rs b/validator/src/validation/cards.rs index 0e6b2e54..cf23428a 100644 --- a/validator/src/validation/cards.rs +++ b/validator/src/validation/cards.rs @@ -1,35 +1,29 @@ use std::borrow::Cow; +#[cfg(feature = "card")] use card_validate::Validate as CardValidate; -#[must_use] -pub fn validate_credit_card(card: T) -> bool -{ - card.validate_credit_card() -} - pub trait ValidateCreditCard { - #[must_use] fn validate_credit_card(&self) -> bool { - let card_string = self.to_credit_card_string(); + let card_string = self.as_credit_card_string(); CardValidate::from(&card_string).is_ok() } - fn to_credit_card_string(&self) -> Cow; + fn as_credit_card_string(&self) -> Cow; } impl> ValidateCreditCard for T { - fn to_credit_card_string(&self) -> Cow { + fn as_credit_card_string(&self) -> Cow { Cow::from(self.as_ref()) } } #[cfg(test)] +#[cfg(feature = "card")] mod tests { + use super::ValidateCreditCard; use std::borrow::Cow; - use super::validate_credit_card; - #[test] fn test_credit_card() { let tests = vec![ @@ -40,19 +34,19 @@ mod tests { ]; for (input, expected) in tests { - assert_eq!(validate_credit_card(input), expected); + assert_eq!(input.validate_credit_card(), expected); } } #[test] fn test_credit_card_cow() { let test: Cow<'static, str> = "4539571147647251".into(); - assert!(validate_credit_card(test)); + assert!(test.validate_credit_card()); let test: Cow<'static, str> = String::from("4539571147647251").into(); - assert!(validate_credit_card(test)); + assert!(test.validate_credit_card()); let test: Cow<'static, str> = "5236313877109141".into(); - assert!(!validate_credit_card(test)); + assert!(!test.validate_credit_card()); let test: Cow<'static, str> = String::from("5236313877109141").into(); - assert!(!validate_credit_card(test)); + assert!(!test.validate_credit_card()); } } diff --git a/validator/src/validation/contains.rs b/validator/src/validation/contains.rs index 7cd274d4..d8235af2 100644 --- a/validator/src/validation/contains.rs +++ b/validator/src/validation/contains.rs @@ -1,11 +1,189 @@ -use crate::traits::Contains; +use std::borrow::Cow; +use std::collections::HashMap; +use std::hash::BuildHasher; -/// Validates whether the value contains the needle -/// The value needs to implement the Contains trait, which is implement on String, str and Hashmap -/// by default. -#[must_use] -pub fn validate_contains(val: T, needle: &str) -> bool { - val.has_element(needle) +pub trait ValidateContains { + fn validate_contains(&self, needle: &str) -> bool; +} + +impl ValidateContains for String { + fn validate_contains(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl ValidateContains for Option { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + v.contains(needle) + } else { + true + } + } +} + +impl ValidateContains for Option> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + if let Some(v) = v { + v.contains(needle) + } else { + true + } + } else { + true + } + } +} + +impl ValidateContains for &String { + fn validate_contains(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl ValidateContains for Option<&String> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + v.contains(needle) + } else { + true + } + } +} + +impl ValidateContains for Option> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + if let Some(v) = v { + v.contains(needle) + } else { + true + } + } else { + true + } + } +} + +impl<'a> ValidateContains for &'a str { + fn validate_contains(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl<'a> ValidateContains for Option<&'a str> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + v.contains(needle) + } else { + true + } + } +} + +impl<'a> ValidateContains for Option> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + if let Some(v) = v { + v.contains(needle) + } else { + true + } + } else { + true + } + } +} + +impl<'a> ValidateContains for Cow<'a, str> { + fn validate_contains(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl<'a> ValidateContains for Option> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + v.contains(needle) + } else { + true + } + } +} + +impl<'a> ValidateContains for Option>> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + if let Some(v) = v { + v.contains(needle) + } else { + true + } + } else { + true + } + } +} + +impl ValidateContains for HashMap { + fn validate_contains(&self, needle: &str) -> bool { + self.contains_key(needle) + } +} + +impl ValidateContains for Option> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + v.contains_key(needle) + } else { + true + } + } +} + +impl ValidateContains for Option>> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + if let Some(v) = v { + v.contains_key(needle) + } else { + true + } + } else { + true + } + } +} + +impl<'a, S, H: BuildHasher> ValidateContains for &'a HashMap { + fn validate_contains(&self, needle: &str) -> bool { + self.contains_key(needle) + } +} + +impl<'a, S, H: BuildHasher> ValidateContains for Option<&'a HashMap> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + v.contains_key(needle) + } else { + true + } + } +} + +impl<'a, S, H: BuildHasher> ValidateContains for Option>> { + fn validate_contains(&self, needle: &str) -> bool { + if let Some(v) = self { + if let Some(v) = v { + v.contains_key(needle) + } else { + true + } + } else { + true + } + } } #[cfg(test)] @@ -17,41 +195,41 @@ mod tests { #[test] fn test_validate_contains_string() { - assert!(validate_contains("hey", "e")); + assert!("hey".validate_contains("e")); } #[test] fn test_validate_contains_string_can_fail() { - assert!(!validate_contains("hey", "o")); + assert!(!"hey".validate_contains("o")); } #[test] fn test_validate_contains_hashmap_key() { let mut map = HashMap::new(); map.insert("hey".to_string(), 1); - assert!(validate_contains(map, "hey")); + assert!(map.validate_contains("hey")); } #[test] fn test_validate_contains_hashmap_key_can_fail() { let mut map = HashMap::new(); map.insert("hey".to_string(), 1); - assert!(!validate_contains(map, "bob")); + assert!(!map.validate_contains("bob")); } #[test] fn test_validate_contains_cow() { let test: Cow<'static, str> = "hey".into(); - assert!(validate_contains(test, "e")); + assert!(test.validate_contains("e")); let test: Cow<'static, str> = String::from("hey").into(); - assert!(validate_contains(test, "e")); + assert!(test.validate_contains("e")); } #[test] fn test_validate_contains_cow_can_fail() { let test: Cow<'static, str> = "hey".into(); - assert!(!validate_contains(test, "o")); + assert!(!test.validate_contains("o")); let test: Cow<'static, str> = String::from("hey").into(); - assert!(!validate_contains(test, "o")); + assert!(!test.validate_contains("o")); } } diff --git a/validator/src/validation/does_not_contain.rs b/validator/src/validation/does_not_contain.rs index 06d34def..ed9a5066 100644 --- a/validator/src/validation/does_not_contain.rs +++ b/validator/src/validation/does_not_contain.rs @@ -1,11 +1,16 @@ -use crate::traits::Contains; - -/// Validates whether the value does not contain the needle -/// The value needs to implement the Contains trait, which is implement on String, str and Hashmap -/// by default. -#[must_use] -pub fn validate_does_not_contain(val: T, needle: &str) -> bool { - !val.has_element(needle) +use crate::ValidateContains; + +pub trait ValidateDoesNotContain { + fn validate_does_not_contain(&self, needle: &str) -> bool; +} + +impl ValidateDoesNotContain for T +where + T: ValidateContains, +{ + fn validate_does_not_contain(&self, needle: &str) -> bool { + !self.validate_contains(needle) + } } #[cfg(test)] @@ -17,41 +22,41 @@ mod tests { #[test] fn test_validate_does_not_contain_string() { - assert_eq!(validate_does_not_contain("hey", "e"), false); + assert!("hey".validate_does_not_contain("g")); } #[test] fn test_validate_does_not_contain_string_can_fail() { - assert!(validate_does_not_contain("hey", "o")); + assert!(!"hey".validate_does_not_contain("e")); } #[test] fn test_validate_does_not_contain_hashmap_key() { let mut map = HashMap::new(); map.insert("hey".to_string(), 1); - assert_eq!(validate_does_not_contain(map, "hey"), false); + assert!(map.validate_does_not_contain("bob")); } #[test] fn test_validate_does_not_contain_hashmap_key_can_fail() { let mut map = HashMap::new(); map.insert("hey".to_string(), 1); - assert!(validate_does_not_contain(map, "bob")); + assert!(!map.validate_does_not_contain("hey")); } #[test] fn test_validate_does_not_contain_cow() { let test: Cow<'static, str> = "hey".into(); - assert_eq!(validate_does_not_contain(test, "e"), false); + assert!(test.validate_does_not_contain("b")); let test: Cow<'static, str> = String::from("hey").into(); - assert_eq!(validate_does_not_contain(test, "e"), false); + assert!(test.validate_does_not_contain("b")); } #[test] fn test_validate_does_not_contain_cow_can_fail() { let test: Cow<'static, str> = "hey".into(); - assert!(validate_does_not_contain(test, "o")); + assert!(!test.validate_does_not_contain("e")); let test: Cow<'static, str> = String::from("hey").into(); - assert!(validate_does_not_contain(test, "o")); + assert!(!test.validate_does_not_contain("e")); } } diff --git a/validator/src/validation/email.rs b/validator/src/validation/email.rs index 8250013c..22624ee0 100644 --- a/validator/src/validation/email.rs +++ b/validator/src/validation/email.rs @@ -3,7 +3,7 @@ use lazy_static::lazy_static; use regex::Regex; use std::borrow::Cow; -use crate::{validation::ip::validate_ip, HasLen}; +use crate::{HasLen, ValidateIp}; lazy_static! { // Regex from the specs @@ -17,14 +17,6 @@ lazy_static! { static ref EMAIL_LITERAL_RE: Regex = Regex::new(r"\[([a-fA-F0-9:\.]+)\]\z").unwrap(); } -/// Validates whether the given string is an email based on the [HTML5 spec](https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address). -/// [RFC 5322](https://tools.ietf.org/html/rfc5322) is not practical in most circumstances and allows email addresses -/// that are unfamiliar to most users. -#[must_use] -pub fn validate_email(val: T) -> bool { - val.validate_email() -} - /// Checks if the domain is a valid domain and if not, check whether it's an IP #[must_use] fn validate_domain_part(domain_part: &str) -> bool { @@ -35,16 +27,19 @@ fn validate_domain_part(domain_part: &str) -> bool { // maybe we have an ip as a domain? match EMAIL_LITERAL_RE.captures(domain_part) { Some(caps) => match caps.get(1) { - Some(c) => validate_ip(c.as_str()), + Some(c) => c.as_str().validate_ip(), None => false, }, None => false, } } +/// Validates whether the given string is an email based on the [HTML5 spec](https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address). +/// [RFC 5322](https://tools.ietf.org/html/rfc5322) is not practical in most circumstances and allows email addresses +/// that are unfamiliar to most users. pub trait ValidateEmail { fn validate_email(&self) -> bool { - let val = self.to_email_string(); + let val = if let Some(v) = self.as_email_string() { v } else { return true }; if val.is_empty() || !val.contains('@') { return false; @@ -77,12 +72,94 @@ pub trait ValidateEmail { true } - fn to_email_string(&self) -> Cow; + fn as_email_string(&self) -> Option>; +} + +impl ValidateEmail for String { + fn as_email_string(&self) -> Option> { + Some(Cow::from(self)) + } +} + +impl ValidateEmail for Option { + fn as_email_string(&self) -> Option> { + self.as_ref().map(Cow::from) + } +} + +impl ValidateEmail for Option> { + fn as_email_string(&self) -> Option> { + if let Some(u) = self { + u.as_ref().map(Cow::from) + } else { + None + } + } +} + +impl ValidateEmail for &String { + fn as_email_string(&self) -> Option> { + Some(Cow::from(self.as_str())) + } +} + +impl ValidateEmail for Option<&String> { + fn as_email_string(&self) -> Option> { + self.as_ref().map(|u| Cow::from(*u)) + } +} + +impl ValidateEmail for Option> { + fn as_email_string(&self) -> Option> { + if let Some(u) = self { + u.as_ref().map(|u| Cow::from(*u)) + } else { + None + } + } +} + +impl<'a> ValidateEmail for &'a str { + fn as_email_string(&self) -> Option> { + Some(Cow::from(*self)) + } +} + +impl<'a> ValidateEmail for Option<&'a str> { + fn as_email_string(&self) -> Option> { + self.as_ref().map(|u| Cow::from(*u)) + } } -impl> ValidateEmail for T { - fn to_email_string(&self) -> Cow<'_, str> { - Cow::from(self.as_ref()) +impl<'a> ValidateEmail for Option> { + fn as_email_string(&self) -> Option> { + if let Some(u) = self { + u.as_ref().map(|u| Cow::from(*u)) + } else { + None + } + } +} + +impl ValidateEmail for Cow<'_, str> { + fn as_email_string(&self) -> Option> { + Some(self.clone()) + } +} + +impl ValidateEmail for Option> { + fn as_email_string(&self) -> Option> { + self.as_ref().map(|u| u.clone()) + } +} + +impl ValidateEmail for Option>> { + fn as_email_string(&self) -> Option> { + if let Some(u) = self { + u.as_ref().map(|u| u.clone()) + } else { + None + } } } @@ -90,7 +167,7 @@ impl> ValidateEmail for T { mod tests { use std::borrow::Cow; - use super::validate_email; + use crate::ValidateEmail; #[test] fn test_validate_email() { @@ -150,7 +227,7 @@ mod tests { for (input, expected) in tests { // println!("{} - {}", input, expected); assert_eq!( - validate_email(input), + input.validate_email(), expected, "Email `{}` was not classified correctly", input @@ -161,22 +238,22 @@ mod tests { #[test] fn test_validate_email_cow() { let test: Cow<'static, str> = "email@here.com".into(); - assert!(validate_email(test)); + assert!(test.validate_email()); let test: Cow<'static, str> = String::from("email@here.com").into(); - assert!(validate_email(test)); + assert!(test.validate_email()); let test: Cow<'static, str> = "a@[127.0.0.1]\n".into(); - assert!(!validate_email(test)); + assert!(!test.validate_email()); let test: Cow<'static, str> = String::from("a@[127.0.0.1]\n").into(); - assert!(!validate_email(test)); + assert!(!test.validate_email()); } #[test] fn test_validate_email_rfc5321() { // 65 character local part let test = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@mail.com"; - assert_eq!(validate_email(test), false); + assert_eq!(test.validate_email(), false); // 256 character domain part let test = "a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com"; - assert_eq!(validate_email(test), false); + assert_eq!(test.validate_email(), false); } } diff --git a/validator/src/validation/ip.rs b/validator/src/validation/ip.rs index cae95c08..3c6e10b9 100644 --- a/validator/src/validation/ip.rs +++ b/validator/src/validation/ip.rs @@ -1,40 +1,37 @@ -use std::borrow::Cow; use std::net::IpAddr; use std::str::FromStr; -/// Validates whether the given string is an IP V4 -#[must_use] -pub fn validate_ip_v4<'a, T>(val: T) -> bool -where - T: Into>, -{ - IpAddr::from_str(val.into().as_ref()).map_or(false, |i| i.is_ipv4()) +pub trait ValidateIp { + /// Validates whether the given string is an IP V4 + fn validate_ipv4(&self) -> bool; + /// Validates whether the given string is an IP V6 + fn validate_ipv6(&self) -> bool; + /// Validates whether the given string is an IP + fn validate_ip(&self) -> bool; } -/// Validates whether the given string is an IP V6 -#[must_use] -pub fn validate_ip_v6<'a, T>(val: T) -> bool +impl ValidateIp for T where - T: Into>, + T: ToString, { - IpAddr::from_str(val.into().as_ref()).map_or(false, |i| i.is_ipv6()) -} + fn validate_ipv4(&self) -> bool { + IpAddr::from_str(&self.to_string()).map_or(false, |i| i.is_ipv4()) + } -/// Validates whether the given string is an IP -#[must_use] -pub fn validate_ip<'a, T>(val: T) -> bool -where - T: Into>, -{ - IpAddr::from_str(val.into().as_ref()).is_ok() + fn validate_ipv6(&self) -> bool { + IpAddr::from_str(&self.to_string()).map_or(false, |i| i.is_ipv6()) + } + + fn validate_ip(&self) -> bool { + IpAddr::from_str(&self.to_string()).is_ok() + } } #[cfg(test)] mod tests { + use super::ValidateIp; use std::borrow::Cow; - use super::{validate_ip, validate_ip_v4, validate_ip_v6}; - #[test] fn test_validate_ip() { let tests = vec![ @@ -50,20 +47,20 @@ mod tests { ]; for (input, expected) in tests { - assert_eq!(validate_ip(input), expected); + assert_eq!(input.validate_ip(), expected); } } #[test] fn test_validate_ip_cow() { let test: Cow<'static, str> = "1.1.1.1".into(); - assert!(validate_ip(test)); + assert!(test.validate_ip()); let test: Cow<'static, str> = String::from("1.1.1.1").into(); - assert!(validate_ip(test)); + assert!(test.validate_ip()); let test: Cow<'static, str> = "2a02::223:6cff :fe8a:2e8a".into(); - assert!(!validate_ip(test)); + assert!(!test.validate_ip()); let test: Cow<'static, str> = String::from("2a02::223:6cff :fe8a:2e8a").into(); - assert!(!validate_ip(test)); + assert!(!test.validate_ip()); } #[test] @@ -81,20 +78,20 @@ mod tests { ]; for (input, expected) in tests { - assert_eq!(validate_ip_v4(input), expected); + assert_eq!(input.validate_ipv4(), expected); } } #[test] fn test_validate_ip_v4_cow() { let test: Cow<'static, str> = "1.1.1.1".into(); - assert!(validate_ip_v4(test)); + assert!(test.validate_ipv4()); let test: Cow<'static, str> = String::from("1.1.1.1").into(); - assert!(validate_ip_v4(test)); + assert!(test.validate_ipv4()); let test: Cow<'static, str> = "٧.2٥.3٣.243".into(); - assert!(!validate_ip_v4(test)); + assert!(!test.validate_ipv4()); let test: Cow<'static, str> = String::from("٧.2٥.3٣.243").into(); - assert!(!validate_ip_v4(test)); + assert!(!test.validate_ipv4()); } #[test] @@ -123,19 +120,19 @@ mod tests { ]; for (input, expected) in tests { - assert_eq!(validate_ip_v6(input), expected); + assert_eq!(input.validate_ipv6(), expected); } } #[test] fn test_validate_ip_v6_cow() { let test: Cow<'static, str> = "fe80::223:6cff:fe8a:2e8a".into(); - assert!(validate_ip_v6(test)); + assert!(test.validate_ipv6()); let test: Cow<'static, str> = String::from("fe80::223:6cff:fe8a:2e8a").into(); - assert!(validate_ip_v6(test)); + assert!(test.validate_ipv6()); let test: Cow<'static, str> = "::ffff:zzzz:0a0a".into(); - assert!(!validate_ip_v6(test)); + assert!(!test.validate_ipv6()); let test: Cow<'static, str> = String::from("::ffff:zzzz:0a0a").into(); - assert!(!validate_ip_v6(test)); + assert!(!test.validate_ipv6()); } } diff --git a/validator/src/validation/length.rs b/validator/src/validation/length.rs index 2b2b3d8e..fdbb9957 100644 --- a/validator/src/validation/length.rs +++ b/validator/src/validation/length.rs @@ -11,149 +11,468 @@ use indexmap::{IndexMap, IndexSet}; /// /// If you apply it on String, don't forget that the length can be different /// from the number of visual characters for Unicode -#[must_use] -pub fn validate_length( - value: T, - min: Option, - max: Option, - equal: Option, -) -> bool { - value.validate_length(min, max, equal) -} - -pub trait ValidateLength { - fn validate_length(&self, min: Option, max: Option, equal: Option) -> bool { - let length = self.length(); - - if let Some(eq) = equal { - return length == eq; - } else { - if let Some(m) = min { - if length < m { - return false; +pub trait ValidateLength +where + T: PartialEq + PartialOrd, +{ + fn validate_length(&self, min: Option, max: Option, equal: Option) -> bool { + if let Some(length) = self.length() { + if let Some(eq) = equal { + return length == eq; + } else { + if let Some(m) = min { + if length < m { + return false; + } } - } - if let Some(m) = max { - if length > m { - return false; + if let Some(m) = max { + if length > m { + return false; + } } } + true + } else { + true + } + } + + fn length(&self) -> Option; +} + +impl ValidateLength for String { + fn length(&self) -> Option { + Some(self.chars().count() as u64) + } +} + +impl ValidateLength for Option { + fn length(&self) -> Option { + self.as_ref().map(|s| s.chars().count() as u64) + } +} + +impl ValidateLength for Option> { + fn length(&self) -> Option { + if let Some(s) = self { + s.as_ref().map(|s| s.chars().count() as u64) + } else { + None + } + } +} + +impl<'a> ValidateLength for &'a String { + fn length(&self) -> Option { + Some(self.chars().count() as u64) + } +} + +impl<'a> ValidateLength for Option<&'a String> { + fn length(&self) -> Option { + self.as_ref().map(|s| s.chars().count() as u64) + } +} + +impl<'a> ValidateLength for Option> { + fn length(&self) -> Option { + self.flatten().map(|s| s.chars().count() as u64) + } +} + +impl<'a> ValidateLength for &'a str { + fn length(&self) -> Option { + Some(self.chars().count() as u64) + } +} + +impl<'a> ValidateLength for Option<&'a str> { + fn length(&self) -> Option { + self.as_ref().map(|s| s.chars().count() as u64) + } +} + +impl<'a> ValidateLength for Option> { + fn length(&self) -> Option { + self.flatten().map(|s| s.chars().count() as u64) + } +} + +impl<'a> ValidateLength for Cow<'a, str> { + fn length(&self) -> Option { + Some(self.chars().count() as u64) + } +} + +impl<'a> ValidateLength for Option> { + fn length(&self) -> Option { + self.as_ref().map(|s| s.chars().count() as u64) + } +} + +impl<'a> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(s) = self { + s.as_ref().map(|s| s.chars().count() as u64) + } else { + None } + } +} + +impl ValidateLength for Vec { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +impl ValidateLength for Option> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +impl ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} - true +impl<'a, T> ValidateLength for &'a Vec { + fn length(&self) -> Option { + Some(self.len() as u64) } +} - fn length(&self) -> u64; +impl<'a, T> ValidateLength for Option<&'a Vec> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } } -impl ValidateLength for String { - fn length(&self) -> u64 { - self.chars().count() as u64 +impl<'a, T> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } } } -impl<'a> ValidateLength for &'a String { - fn length(&self) -> u64 { - self.chars().count() as u64 +impl ValidateLength for &[T] { + fn length(&self) -> Option { + Some(self.len() as u64) } } -impl<'a> ValidateLength for &'a str { - fn length(&self) -> u64 { - self.chars().count() as u64 +impl ValidateLength for Option<&[T]> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) } } -impl<'a> ValidateLength for Cow<'a, str> { - fn length(&self) -> u64 { - self.chars().count() as u64 +impl ValidateLength for Option> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } } } -impl ValidateLength for Vec { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for [T; N] { + fn length(&self) -> Option { + Some(N as u64) } } -impl<'a, T> ValidateLength for &'a Vec { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for Option<[T; N]> { + fn length(&self) -> Option { + Some(N as u64) } } -impl ValidateLength for &[T] { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for Option> { + fn length(&self) -> Option { + Some(N as u64) } } -impl ValidateLength for [T; N] { - fn length(&self) -> u64 { - N as u64 +impl ValidateLength for &[T; N] { + fn length(&self) -> Option { + Some(N as u64) } } -impl ValidateLength for &[T; N] { - fn length(&self) -> u64 { - N as u64 +impl ValidateLength for Option<&[T; N]> { + fn length(&self) -> Option { + Some(N as u64) } } -impl<'a, K, V, S> ValidateLength for &'a HashMap { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for Option> { + fn length(&self) -> Option { + Some(N as u64) } } -impl ValidateLength for HashMap { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for HashMap { + fn length(&self) -> Option { + Some(self.len() as u64) } } -impl<'a, T, S> ValidateLength for &'a HashSet { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for Option> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) } } -impl<'a, K, V> ValidateLength for &'a BTreeMap { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +impl<'a, K, V, S> ValidateLength for &'a HashMap { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +impl<'a, K, V, S> ValidateLength for Option<&'a HashMap> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +impl<'a, K, V, S> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +impl ValidateLength for HashSet { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +impl ValidateLength for Option> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +impl ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } } } -impl<'a, T> ValidateLength for &'a BTreeSet { - fn length(&self) -> u64 { - self.len() as u64 +impl<'a, T, S> ValidateLength for &'a HashSet { + fn length(&self) -> Option { + Some(self.len() as u64) } } -impl ValidateLength for BTreeSet { - fn length(&self) -> u64 { - self.len() as u64 +impl<'a, T, S> ValidateLength for Option<&'a HashSet> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +impl<'a, T, S> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +impl<'a, K, V> ValidateLength for &'a BTreeMap { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +impl<'a, K, V> ValidateLength for Option<&'a BTreeMap> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +impl<'a, K, V> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +impl ValidateLength for BTreeSet { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +impl ValidateLength for Option> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +impl ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +impl<'a, T> ValidateLength for &'a BTreeSet { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +impl<'a, T> ValidateLength for Option<&'a BTreeSet> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +impl<'a, T> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } } } #[cfg(feature = "indexmap")] -impl<'a, K, V> ValidateLength for &'a IndexMap { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for IndexMap { + fn length(&self) -> Option { + Some(self.len() as u64) } } #[cfg(feature = "indexmap")] -impl<'a, T> ValidateLength for &'a IndexSet { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for Option> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) } } #[cfg(feature = "indexmap")] -impl ValidateLength for IndexSet { - fn length(&self) -> u64 { - self.len() as u64 +impl ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +#[cfg(feature = "indexmap")] +impl<'a, K, V> ValidateLength for &'a IndexMap { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +#[cfg(feature = "indexmap")] +impl<'a, K, V> ValidateLength for Option<&'a IndexMap> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +#[cfg(feature = "indexmap")] +impl<'a, K, V> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +#[cfg(feature = "indexmap")] +impl ValidateLength for IndexSet { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +#[cfg(feature = "indexmap")] +impl ValidateLength for Option> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +#[cfg(feature = "indexmap")] +impl ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } + } +} + +#[cfg(feature = "indexmap")] +impl<'a, T> ValidateLength for &'a IndexSet { + fn length(&self) -> Option { + Some(self.len() as u64) + } +} + +#[cfg(feature = "indexmap")] +impl<'a, T> ValidateLength for Option<&'a IndexSet> { + fn length(&self) -> Option { + self.as_ref().map(|v| v.len() as u64) + } +} + +#[cfg(feature = "indexmap")] +impl<'a, T> ValidateLength for Option>> { + fn length(&self) -> Option { + if let Some(v) = self { + v.as_ref().map(|v| v.len() as u64) + } else { + None + } } } @@ -161,45 +480,45 @@ impl ValidateLength for IndexSet { mod tests { use std::borrow::Cow; - use crate::{validate_length, validation::length::ValidateLength}; + use super::ValidateLength; #[test] fn test_validate_length_equal_overrides_min_max() { - assert!(validate_length("hello", Some(1), Some(2), Some(5))); + assert!("hello".validate_length(Some(1), Some(2), Some(5))); } #[test] fn test_validate_length_string_min_max() { - assert!(validate_length("hello", Some(1), Some(10), None)); + assert!("hello".validate_length(Some(1), Some(10), None)); } #[test] fn test_validate_length_string_min_only() { - assert!(!validate_length("hello", Some(10), None, None)); + assert!(!"hello".validate_length(Some(10), None, None)); } #[test] fn test_validate_length_string_max_only() { - assert!(!validate_length("hello", None, Some(1), None)); + assert!(!"hello".validate_length(None, Some(1), None)); } #[test] fn test_validate_length_cow() { let test: Cow<'static, str> = "hello".into(); - assert!(validate_length(test, None, None, Some(5))); + assert!(test.validate_length(None, None, Some(5))); let test: Cow<'static, str> = String::from("hello").into(); - assert!(validate_length(test, None, None, Some(5))); + assert!(test.validate_length(None, None, Some(5))); } #[test] fn test_validate_length_vec() { - assert!(validate_length(vec![1, 2, 3], None, None, Some(3))); + assert!(vec![1, 2, 3].validate_length(None, None, Some(3))); } #[test] fn test_validate_length_unicode_chars() { - assert!(validate_length("日本", None, None, Some(2))); + assert!("日本".validate_length(None, None, Some(2))); } #[test] diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs index 28cb3762..02e2c62d 100644 --- a/validator/src/validation/mod.rs +++ b/validator/src/validation/mod.rs @@ -6,8 +6,10 @@ pub mod email; pub mod ip; pub mod length; pub mod must_match; +pub mod nested; #[cfg(feature = "unic")] pub mod non_control_character; pub mod range; +pub mod regex; pub mod required; pub mod urls; diff --git a/validator/src/validation/nested.rs b/validator/src/validation/nested.rs new file mode 100644 index 00000000..584f649d --- /dev/null +++ b/validator/src/validation/nested.rs @@ -0,0 +1,195 @@ +use crate::{ValidateArgs, ValidationErrors, ValidationErrorsKind}; +use std::collections::{BTreeMap, HashMap, HashSet}; +pub trait ValidateNested<'v_a> { + type Args; + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors>; +} + +impl<'v_a, T, U> ValidateNested<'v_a> for &T +where + T: ValidateNested<'v_a, Args = U>, +{ + type Args = U; + + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors> { + T::validate_nested(self, field_name, args) + } +} + +impl<'v_a, T, U> ValidateNested<'v_a> for Option +where + T: ValidateNested<'v_a, Args = U>, +{ + type Args = U; + + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors> { + if let Some(nested) = self { + nested.validate_nested(field_name, args) + } else { + Ok(()) + } + } +} + +impl<'v_a, T, U> ValidateNested<'v_a> for Vec +where + T: ValidateArgs<'v_a, Args = U> + ValidateNested<'v_a, Args = U>, + U: Clone, +{ + type Args = U; + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors> { + let mut vec_err: BTreeMap> = BTreeMap::new(); + + for (index, item) in self.iter().enumerate() { + if let Err(e) = item.validate_with_args(args.clone()) { + vec_err.insert(index, Box::new(e)); + } + } + + let err_kind = ValidationErrorsKind::List(vec_err); + let errors = ValidationErrors(HashMap::from([(field_name, err_kind)])); + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +impl<'v_a, T, U> ValidateNested<'v_a> for &[T] +where + T: ValidateArgs<'v_a, Args = U> + ValidateNested<'v_a, Args = U>, + U: Clone, +{ + type Args = U; + + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors> { + let mut vec_err: BTreeMap> = BTreeMap::new(); + + for (index, item) in self.iter().enumerate() { + if let Err(e) = item.validate_with_args(args.clone()) { + vec_err.insert(index, Box::new(e)); + } + } + + let err_kind = ValidationErrorsKind::List(vec_err); + let errors = ValidationErrors(HashMap::from([(field_name, err_kind)])); + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +impl<'v_a, T, const N: usize, U> ValidateNested<'v_a> for [T; N] +where + T: ValidateArgs<'v_a, Args = U> + ValidateNested<'v_a, Args = U>, + U: Clone, +{ + type Args = U; + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors> { + let mut vec_err: BTreeMap> = BTreeMap::new(); + + for (index, item) in self.iter().enumerate() { + if let Err(e) = item.validate_with_args(args.clone()) { + vec_err.insert(index, Box::new(e)); + } + } + + let err_kind = ValidationErrorsKind::List(vec_err); + let errors = ValidationErrors(HashMap::from([(field_name, err_kind)])); + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +impl<'v_a, K, V, S, U> ValidateNested<'v_a> for HashMap +where + V: ValidateArgs<'v_a, Args = U> + ValidateNested<'v_a, Args = U>, + U: Clone, +{ + type Args = U; + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors> { + let mut vec_err: BTreeMap> = BTreeMap::new(); + + for (index, (_key, value)) in self.iter().enumerate() { + if let Err(e) = value.validate_with_args(args.clone()) { + vec_err.insert(index, Box::new(e)); + } + } + + let err_kind = ValidationErrorsKind::List(vec_err); + let errors = ValidationErrors(HashMap::from([(field_name, err_kind)])); + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +impl<'v_a, T, S, U> ValidateNested<'v_a> for HashSet +where + T: ValidateArgs<'v_a, Args = U> + ValidateNested<'v_a, Args = U>, + U: Clone, +{ + type Args = U; + fn validate_nested( + &self, + field_name: &'static str, + args: Self::Args, + ) -> Result<(), ValidationErrors> { + let mut vec_err: BTreeMap> = BTreeMap::new(); + + for (index, value) in self.iter().enumerate() { + if let Err(e) = value.validate_with_args(args.clone()) { + vec_err.insert(index, Box::new(e)); + } + } + + let err_kind = ValidationErrorsKind::List(vec_err); + let errors = ValidationErrors(HashMap::from([(field_name, err_kind)])); + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} diff --git a/validator/src/validation/non_control_character.rs b/validator/src/validation/non_control_character.rs index b539bbfc..b12a2812 100644 --- a/validator/src/validation/non_control_character.rs +++ b/validator/src/validation/non_control_character.rs @@ -1,31 +1,26 @@ +#[cfg(feature = "unic")] use unic_ucd_common::control; -#[must_use] -pub fn validate_non_control_character(val: T) -> bool { - val.validate_non_control_character() -} - pub trait ValidateNonControlCharacter { - #[must_use] fn validate_non_control_character(&self) -> bool { - self.to_non_control_character_iterator().all(|code| !control::is_control(code)) + self.as_non_control_character_iterator().all(|code| !control::is_control(code)) } - fn to_non_control_character_iterator(&self) -> Box + '_>; + fn as_non_control_character_iterator(&self) -> Box + '_>; } impl> ValidateNonControlCharacter for T { - fn to_non_control_character_iterator(&self) -> Box + '_> { + fn as_non_control_character_iterator(&self) -> Box + '_> { Box::new(self.as_ref().chars()) } } #[cfg(test)] +#[cfg(feature = "unic")] mod tests { + use super::ValidateNonControlCharacter; use std::borrow::Cow; - use super::validate_non_control_character; - #[test] fn test_non_control_character() { let tests = vec![ @@ -40,19 +35,19 @@ mod tests { ]; for (input, expected) in tests { - assert_eq!(validate_non_control_character(input), expected); + assert_eq!(input.validate_non_control_character(), expected); } } #[test] fn test_non_control_character_cow() { let test: Cow<'static, str> = "आकाश".into(); - assert!(validate_non_control_character(test)); + assert!(test.validate_non_control_character()); let test: Cow<'static, str> = String::from("வானத்தில்").into(); - assert!(validate_non_control_character(test)); + assert!(test.validate_non_control_character()); let test: Cow<'static, str> = "\u{000c}".into(); - assert!(!validate_non_control_character(test)); + assert!(!test.validate_non_control_character()); let test: Cow<'static, str> = String::from("\u{009F}").into(); - assert!(!validate_non_control_character(test)); + assert!(!test.validate_non_control_character()); } } diff --git a/validator/src/validation/range.rs b/validator/src/validation/range.rs index 20fa5e80..ab8a0b78 100644 --- a/validator/src/validation/range.rs +++ b/validator/src/validation/range.rs @@ -1,18 +1,6 @@ /// Validates that the given `value` is inside the defined range. /// The `max`, `min`, `exclusive_max` and `exclusive_min` parameters are /// optional and will only be validated if they are not `None` -/// -#[must_use] -pub fn validate_range>( - value: T, - min: Option, - max: Option, - exclusive_min: Option, - exclusive_max: Option, -) -> bool { - value.validate_range(min, max, exclusive_min, exclusive_max) -} - pub trait ValidateRange { fn validate_range( &self, @@ -22,111 +10,228 @@ pub trait ValidateRange { exclusive_max: Option, ) -> bool { if let Some(max) = max { - if self.greater_than(max) { - return false; + if let Some(gt) = self.greater_than(max) { + if gt { + return false; + } } } if let Some(min) = min { - if self.less_than(min) { - return false; + if let Some(lt) = self.less_than(min) { + if lt { + return false; + } } } if let Some(exclusive_max) = exclusive_max { - if !self.less_than(exclusive_max) { - return false; + if let Some(lt) = self.less_than(exclusive_max) { + if !lt { + return false; + } } } if let Some(exclusive_min) = exclusive_min { - if !self.greater_than(exclusive_min) { - return false; + if let Some(gt) = self.greater_than(exclusive_min) { + if !gt { + return false; + } } } true } - - fn greater_than(&self, max: T) -> bool; - fn less_than(&self, min: T) -> bool; + fn greater_than(&self, max: T) -> Option; + fn less_than(&self, min: T) -> Option; } +pub trait ValidateRangeType {} + impl ValidateRange for T where - T: PartialEq + PartialOrd, + T: PartialEq + PartialOrd + ValidateRangeType, { - fn greater_than(&self, max: T) -> bool { - if self > &max { - return true; - } + fn greater_than(&self, max: T) -> Option { + Some(self > &max) + } + + fn less_than(&self, min: T) -> Option { + Some(self < &min) + } +} + +impl ValidateRange for Option +where + T: PartialEq + PartialOrd + ValidateRangeType, +{ + fn greater_than(&self, max: T) -> Option { + self.as_ref().map(|r| r > &max) + } - false + fn less_than(&self, min: T) -> Option { + self.as_ref().map(|r| r < &min) } +} - fn less_than(&self, min: T) -> bool { - if self < &min { - return true; +impl ValidateRange for Option> +where + T: PartialEq + PartialOrd + ValidateRangeType, +{ + fn greater_than(&self, max: T) -> Option { + if let Some(r) = self { + r.as_ref().map(|r| r > &max) + } else { + None } + } - false + fn less_than(&self, min: T) -> Option { + if let Some(r) = self { + r.as_ref().map(|r| r < &min) + } else { + None + } } } +macro_rules! impl_val_range { + ($t:tt) => { + impl ValidateRange<$t> for $t { + fn greater_than(&self, max: $t) -> Option { + Some(self > &max) + } + + fn less_than(&self, min: $t) -> Option { + Some(self < &min) + } + } + + impl ValidateRange<$t> for Option<$t> { + fn greater_than(&self, max: $t) -> Option { + self.map(|r| r > max) + } + + fn less_than(&self, min: $t) -> Option { + self.map(|r| r < min) + } + } + + impl ValidateRange<$t> for Option> { + fn greater_than(&self, max: $t) -> Option { + self.flatten().map(|r| r > max) + } + + fn less_than(&self, min: $t) -> Option { + self.flatten().map(|r| r < min) + } + } + }; +} + +impl_val_range!(u8); +impl_val_range!(u16); +impl_val_range!(u32); +impl_val_range!(u64); +impl_val_range!(u128); +impl_val_range!(usize); +impl_val_range!(i8); +impl_val_range!(i16); +impl_val_range!(i32); +impl_val_range!(i64); +impl_val_range!(i128); +impl_val_range!(isize); +impl_val_range!(f32); +impl_val_range!(f64); + #[cfg(test)] mod tests { - use super::validate_range; + use crate::validation::range::ValidateRangeType; + + use super::ValidateRange; #[test] fn test_validate_range_generic_ok() { // Unspecified generic type: - assert!(validate_range(10, Some(-10), Some(10), None, None)); - assert!(validate_range(0.0, Some(0.0), Some(10.0), None, None)); + assert!(10.validate_range(Some(-10), Some(10), None, None)); + assert!(0.0.validate_range(Some(0.0), Some(10.0), None, None)); // Specified type: - assert!(validate_range(5u8, Some(0), Some(255), None, None)); - assert!(validate_range(4u16, Some(0), Some(16), None, None)); - assert!(validate_range(6u32, Some(0), Some(23), None, None)); + assert!(5u8.validate_range(Some(0), Some(255), None, None)); + assert!(4u16.validate_range(Some(0), Some(16), None, None)); + assert!(6u32.validate_range(Some(0), Some(23), None, None)); } #[test] fn test_validate_range_generic_fail() { - assert!(!validate_range(5, Some(17), Some(19), None, None)); - assert!(!validate_range(-1.0, Some(0.0), Some(10.0), None, None)); + assert!(!5.validate_range(Some(17), Some(19), None, None)); + assert!(!(-1.0).validate_range(Some(0.0), Some(10.0), None, None)); } #[test] fn test_validate_range_generic_min_only() { - assert!(!validate_range(5, Some(10), None, None, None)); - assert!(validate_range(15, Some(10), None, None, None)); + assert!(!5.validate_range(Some(10), None, None, None)); + assert!(15.validate_range(Some(10), None, None, None)); } #[test] fn test_validate_range_generic_max_only() { - assert!(validate_range(5, None, Some(10), None, None)); - assert!(!validate_range(15, None, Some(10), None, None)); + assert!(5.validate_range(None, Some(10), None, None)); + assert!(!15.validate_range(None, Some(10), None, None)); } #[test] fn test_validate_range_generic_exc_ok() { - assert!(validate_range(6, None, None, Some(5), Some(7))); - assert!(validate_range(0.0001, None, None, Some(0.0), Some(1.0))); + assert!(6.validate_range(None, None, Some(5), Some(7))); + assert!(0.0001.validate_range(None, None, Some(0.0), Some(1.0))); } #[test] fn test_validate_range_generic_exc_fail() { - assert!(!validate_range(5, None, None, Some(5), None)); + assert!(!5.validate_range(None, None, Some(5), None)); } #[test] fn test_validate_range_generic_exclusive_max_only() { - assert!(!validate_range(10, None, None, None, Some(10))); - assert!(validate_range(9, None, None, None, Some(10))); + assert!(!10.validate_range(None, None, None, Some(10))); + assert!(9.validate_range(None, None, None, Some(10))); } #[test] fn test_validate_range_generic_exclusive_min_only() { - assert!(!validate_range(10, None, None, Some(10), None)); - assert!(validate_range(9, None, None, Some(8), None)); + assert!(!10.validate_range(None, None, Some(10), None)); + assert!(9.validate_range(None, None, Some(8), None)); + } + + #[test] + fn test_validate_range_with_enums() { + #[derive(PartialEq, PartialOrd)] + enum Test { + One, + Two, + Three, + Four, + } + + impl ValidateRangeType for Test {} + + assert!(Test::Two.validate_range(Some(Test::One), Some(Test::Three), None, None)); + assert!(!Test::Four.validate_range(Some(Test::One), Some(Test::Three), None, None)); + } + + #[test] + fn test_validate_range_with_option() { + assert!(Some(5).validate_range(Some(1), Some(10), None, None)); + assert!(!Some(11).validate_range(Some(1), Some(10), None, None)); + } + + #[test] + fn test_validate_range_with_none_value() { + let none: Option = None; + let none_none: Option> = None; + assert!(none.validate_range(Some(1), Some(10), None, None)); + assert!(none.validate_range(Some(1), None, None, Some(10))); + assert!(none_none.validate_range(Some(1), Some(10), None, None)); } } diff --git a/validator/src/validation/regex.rs b/validator/src/validation/regex.rs new file mode 100644 index 00000000..70942dcd --- /dev/null +++ b/validator/src/validation/regex.rs @@ -0,0 +1,175 @@ +use std::borrow::Cow; +use std::cell::OnceCell; +use std::sync::{Arc, Mutex, OnceLock}; + +use regex::Regex; + +pub trait AsRegex { + fn as_regex(&self) -> Cow; +} + +impl AsRegex for Regex { + fn as_regex(&self) -> Cow { + Cow::Borrowed(self) + } +} + +impl AsRegex for &Regex { + fn as_regex(&self) -> Cow { + Cow::Borrowed(self) + } +} + +impl AsRegex for &OnceLock { + fn as_regex(&self) -> Cow { + Cow::Borrowed(self.get().unwrap()) + } +} + +impl AsRegex for &Mutex> { + fn as_regex(&self) -> Cow { + Cow::Owned(self.lock().unwrap().get().unwrap().clone()) + } +} + +impl AsRegex for &Mutex> { + fn as_regex(&self) -> Cow { + Cow::Owned(self.lock().unwrap().get().unwrap().clone()) + } +} + +impl AsRegex for &Arc>> { + fn as_regex(&self) -> Cow { + Cow::Owned(self.lock().unwrap().get().unwrap().clone()) + } +} + +impl AsRegex for &Arc>> { + fn as_regex(&self) -> Cow { + Cow::Owned(self.lock().unwrap().get().unwrap().clone()) + } +} + +pub trait ValidateRegex { + fn validate_regex(&self, regex: impl AsRegex) -> bool; +} + +impl ValidateRegex for String { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + regex.as_regex().is_match(self) + } +} + +impl ValidateRegex for Option { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + regex.as_regex().is_match(h) + } else { + true + } + } +} + +impl ValidateRegex for Option> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + if let Some(h) = h { + regex.as_regex().is_match(h) + } else { + true + } + } else { + true + } + } +} + +impl ValidateRegex for &String { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + regex.as_regex().is_match(self) + } +} + +impl ValidateRegex for Option<&String> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + regex.as_regex().is_match(h) + } else { + true + } + } +} + +impl ValidateRegex for Option> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + if let Some(h) = h { + regex.as_regex().is_match(h) + } else { + true + } + } else { + true + } + } +} + +impl ValidateRegex for &str { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + regex.as_regex().is_match(self) + } +} + +impl ValidateRegex for Option<&str> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + regex.as_regex().is_match(h) + } else { + true + } + } +} + +impl ValidateRegex for Option> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + if let Some(h) = h { + regex.as_regex().is_match(h) + } else { + true + } + } else { + true + } + } +} + +impl ValidateRegex for Cow<'_, str> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + regex.as_regex().is_match(self) + } +} + +impl ValidateRegex for Option> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + regex.as_regex().is_match(h) + } else { + true + } + } +} + +impl ValidateRegex for Option>> { + fn validate_regex(&self, regex: impl AsRegex) -> bool { + if let Some(h) = self { + if let Some(h) = h { + regex.as_regex().is_match(h) + } else { + true + } + } else { + true + } + } +} diff --git a/validator/src/validation/required.rs b/validator/src/validation/required.rs index 863aeee3..51b3b79a 100644 --- a/validator/src/validation/required.rs +++ b/validator/src/validation/required.rs @@ -1,9 +1,4 @@ /// Validates whether the given Option is Some -#[must_use] -pub fn validate_required(val: &T) -> bool { - val.is_some() -} - pub trait ValidateRequired { fn validate_required(&self) -> bool { self.is_some() diff --git a/validator/src/validation/urls.rs b/validator/src/validation/urls.rs index a2c05e98..ba088b31 100644 --- a/validator/src/validation/urls.rs +++ b/validator/src/validation/urls.rs @@ -2,22 +2,91 @@ use std::borrow::Cow; use url::Url; /// Validates whether the string given is a url -#[must_use] -pub fn validate_url(val: T) -> bool { - val.validate_url() -} - pub trait ValidateUrl { fn validate_url(&self) -> bool { - Url::parse(&self.to_url_string()).is_ok() + if let Some(u) = self.as_url_string() { + Url::parse(&u).is_ok() + } else { + true + } + } + + fn as_url_string(&self) -> Option>; +} + +impl ValidateUrl for String { + fn as_url_string(&self) -> Option> { + Some(Cow::from(self)) } +} - fn to_url_string(&self) -> Cow; +impl ValidateUrl for Option { + fn as_url_string(&self) -> Option> { + self.as_ref().map(Cow::from) + } +} + +impl ValidateUrl for Option> { + fn as_url_string(&self) -> Option> { + if let Some(u) = self { + u.as_ref().map(Cow::from) + } else { + None + } + } +} + +impl ValidateUrl for &String { + fn as_url_string(&self) -> Option> { + Some(Cow::from(self.as_str())) + } +} + +impl ValidateUrl for Option<&String> { + fn as_url_string(&self) -> Option> { + self.as_ref().map(|u| Cow::from(*u)) + } +} + +impl ValidateUrl for Option> { + fn as_url_string(&self) -> Option> { + self.flatten().map(Cow::from) + } +} + +impl<'a> ValidateUrl for &'a str { + fn as_url_string(&self) -> Option> { + Some(Cow::from(*self)) + } +} + +impl<'a> ValidateUrl for Option<&'a str> { + fn as_url_string(&self) -> Option> { + self.as_ref().map(|u| Cow::from(*u)) + } +} + +impl<'a> ValidateUrl for Option> { + fn as_url_string(&self) -> Option> { + self.flatten().map(Cow::from) + } +} + +impl ValidateUrl for Cow<'_, str> { + fn as_url_string(&self) -> Option> { + Some(self.clone()) + } +} + +impl ValidateUrl for Option> { + fn as_url_string(&self) -> Option> { + self.as_ref().cloned() + } } -impl> ValidateUrl for T { - fn to_url_string(&self) -> Cow<'_, str> { - Cow::from(self.as_ref()) +impl ValidateUrl for Option>> { + fn as_url_string(&self) -> Option> { + self.as_ref().cloned().flatten() } } @@ -25,7 +94,7 @@ impl> ValidateUrl for T { mod tests { use std::borrow::Cow; - use super::validate_url; + use super::ValidateUrl; #[test] fn test_validate_url() { @@ -37,19 +106,19 @@ mod tests { ]; for (url, expected) in tests { - assert_eq!(validate_url(url), expected); + assert_eq!(url.validate_url(), expected); } } #[test] fn test_validate_url_cow() { let test: Cow<'static, str> = "http://localhost:80".into(); - assert!(validate_url(test)); + assert!(test.validate_url()); let test: Cow<'static, str> = String::from("http://localhost:80").into(); - assert!(validate_url(test)); + assert!(test.validate_url()); let test: Cow<'static, str> = "http".into(); - assert!(!validate_url(test)); + assert!(!test.validate_url()); let test: Cow<'static, str> = String::from("http").into(); - assert!(!validate_url(test)); + assert!(!test.validate_url()); } } diff --git a/validator_derive/Cargo.toml b/validator_derive/Cargo.toml index 8e784642..60afec73 100644 --- a/validator_derive/Cargo.toml +++ b/validator_derive/Cargo.toml @@ -7,24 +7,17 @@ description = "Macros 1.1 implementation of #[derive(Validate)]" homepage = "https://github.com/Keats/validator" repository = "https://github.com/Keats/validator" keywords = ["validation", "api", "validator"] -edition = "2018" +edition = "2021" readme = "../README.md" [lib] proc-macro = true -[features] -card = ["validator_types/card"] -unic = ["validator_types/unic"] - [dependencies] -syn = { version = "1", features = ["extra-traits"] } +syn = "2" quote = "1" proc-macro2 = "1" proc-macro-error = "1" -if_chain = "1" -validator_types = { version = "0.16", path = "../validator_types" } -regex = { version = "1.5.5", default-features = false, features = ["std"] } +regex = { version = "1", default-features = false, features = ["std"] } lazy_static = "1" - - +darling = { version = "0.20", features = ["suggestions"] } diff --git a/validator_derive/src/asserts.rs b/validator_derive/src/asserts.rs deleted file mode 100644 index 546dbb06..00000000 --- a/validator_derive/src/asserts.rs +++ /dev/null @@ -1,168 +0,0 @@ -use proc_macro2::Span; -use regex::Regex; - -use lazy_static::lazy_static; -use proc_macro_error::abort; -use syn::spanned::Spanned; - -lazy_static! { - pub static ref COW_TYPE: Regex = Regex::new(r"Cow<'[a-z]+,str>").unwrap(); - pub static ref LEN_TYPE: Regex = - Regex::new(r"(Option<)?((Vec|HashMap|HashSet|BTreeMap|BTreeSet|IndexMap|IndexSet)<|\[)") - .unwrap(); -} - -static CUSTOM_ARG_LIFETIME: &str = "v_a"; - -static CUSTOM_ARG_ALLOWED_COPY_TYPES: [&str; 14] = [ - "usize", "u8", "u16", "u32", "u64", "u128", "isize", "i8", "i16", "i32", "i64", "i128", "f32", - "f64", -]; - -pub static NUMBER_TYPES: [&str; 38] = [ - "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>", -]; - -pub fn assert_type_matches( - field_name: String, - field_type: &str, - field_type2: Option<&String>, - field_attr: &syn::Attribute, -) { - if let Some(t2) = field_type2 { - if field_type != t2 { - abort!(field_attr.span(), "Invalid argument for `must_match` validator of field `{}`: types of field can't match", field_name); - } - } else { - abort!(field_attr.span(), "Invalid argument for `must_match` validator of field `{}`: the other field doesn't exist in struct", field_name); - } -} - -pub fn assert_has_len(field_name: String, type_name: &str, field_type: &syn::Type) { - if let syn::Type::Reference(ref tref) = field_type { - let elem = &tref.elem; - let type_name = format!("{}", quote::quote! { #elem }).replace(' ', ""); - - if type_name == "str" { - return; - } - assert_has_len(field_name, &type_name, elem); - return; - } - - if !type_name.contains("String") - && !type_name.contains("str") - && !LEN_TYPE.is_match(type_name) - // a bit ugly - && !COW_TYPE.is_match(type_name) - { - abort!(field_type.span(), - "Validator `length` can only be used on types `String`, `&str`, Cow<'_,str>, `Vec`, slice, or map/set types (BTree/Hash/Index) but found `{}` for field `{}`", - type_name, field_name - ); - } -} - -pub fn assert_has_range(field_name: String, type_name: &str, field_type: &syn::Type) { - if !NUMBER_TYPES.contains(&type_name) { - abort!( - field_type.span(), - "Validator `range` can only be used on number types but found `{}` for field `{}`", - type_name, - field_name - ); - } -} - -pub fn assert_custom_arg_type(field_span: &Span, field_type: &syn::Type) { - match field_type { - syn::Type::Reference(reference) => { - if let Some(lifetime) = &reference.lifetime { - let lifetime_ident = lifetime.ident.to_string(); - if lifetime_ident != CUSTOM_ARG_LIFETIME { - abort!( - field_span, - "Invalid argument reference: The lifetime `'{}` is not supported. Please use the validator lifetime `'{}`", - lifetime_ident, - CUSTOM_ARG_LIFETIME - ); - } - } else { - abort!( - field_span, - "Invalid argument reference: All references need to use the validator lifetime `'{}`", - CUSTOM_ARG_LIFETIME - ); - } - } - // trigger nested validation - syn::Type::Paren(paren) => { - assert_custom_arg_type(field_span, &paren.elem); - } - syn::Type::Tuple(tuple) => { - tuple.elems.iter().for_each(|x| assert_custom_arg_type(field_span, x)); - } - // assert idents - syn::Type::Path(path) => { - let segments = &path.path.segments; - if segments.len() == 1 { - let ident = &segments.first().unwrap().ident.to_string(); - if CUSTOM_ARG_ALLOWED_COPY_TYPES.contains(&ident.as_str()) { - // A known copy type that can be passed without a reference - return; - } - } - - abort!( - field_span, - "Invalid argument type: All types except numbers and tuples need be passed by reference using the lifetime `'{}`", - CUSTOM_ARG_LIFETIME, - ); - } - // Not allows - _ => { - abort!( - field_span, - "Invalid argument type: Custom arguments only allow tuples, number types and references using the lifetime `'{}` ", - CUSTOM_ARG_LIFETIME, - ); - } - } -} diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index ae80d266..3489f56f 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -1,629 +1,411 @@ -#![recursion_limit = "128"] - -use std::{collections::HashMap, unreachable}; - -use if_chain::if_chain; -use proc_macro2::Span; +use darling::ast::Data; +use darling::util::{Override, WithOriginal}; +use darling::FromDeriveInput; use proc_macro_error::{abort, proc_macro_error}; -use quote::ToTokens; -use quote::{quote, quote_spanned}; -use syn::{parse_quote, spanned::Spanned, GenericParam, Lifetime, LifetimeDef, Type}; - -use asserts::{assert_has_len, assert_has_range, assert_type_matches}; -use lit::*; -use quoting::{quote_schema_validations, quote_validator, FieldQuoter}; -use validation::*; -use validator_types::{CustomArgument, Validator}; - -use crate::asserts::assert_custom_arg_type; - -mod asserts; -mod lit; -mod quoting; -mod validation; - -#[proc_macro_derive(Validate, attributes(validate))] -#[proc_macro_error] -pub fn derive_validation(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let ast = syn::parse(input).unwrap(); - impl_validate(&ast).into() -} - -fn impl_validate(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { - // Collecting the validators - let mut fields_validations = collect_field_validations(ast); - let mut struct_validations = find_struct_validations(&ast.attrs); - let (arg_type, has_arg) = - construct_validator_argument_type(&mut fields_validations, &mut struct_validations); - let (validations, nested_validations) = quote_field_validations(fields_validations); - - let schema_validations = quote_schema_validations(&struct_validations); - - // Struct specific definitions - let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); - - // The Validate trait implementation - let validate_trait_impl = if !has_arg { - quote!( - impl #impl_generics ::validator::Validate for #ident #ty_generics #where_clause { - fn validate(&self) -> ::std::result::Result<(), ::validator::ValidationErrors> { - use ::validator::ValidateArgs; - self.validate_args(()) - } - } - ) - } else { - quote!() - }; +use quote::{quote, ToTokens}; +use syn::{parse_macro_input, DeriveInput, Field, Path, PathArguments}; + +use tokens::cards::credit_card_tokens; +use tokens::contains::contains_tokens; +use tokens::custom::custom_tokens; +use tokens::does_not_contain::does_not_contain_tokens; +use tokens::email::email_tokens; +use tokens::ip::ip_tokens; +use tokens::length::length_tokens; +use tokens::must_match::must_match_tokens; +use tokens::nested::nested_tokens; +use tokens::non_control_character::non_control_char_tokens; +use tokens::range::range_tokens; +use tokens::regex::regex_tokens; +use tokens::required::required_tokens; +use tokens::required_nested::required_nested_tokens; +use tokens::schema::schema_tokens; +use tokens::url::url_tokens; +use types::*; +use utils::quote_use_stmts; + +mod tokens; +mod types; +mod utils; + +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(); + + // Length validation + let length = if let Some(length) = self.length.clone() { + length_tokens(length, &field_name, &field_name_str) + } else { + quote!() + }; - // Adding the validator lifetime 'v_a - let mut expanded_generic = ast.generics.clone(); - expanded_generic - .params - .insert(0, GenericParam::Lifetime(LifetimeDef::new(Lifetime::new("'v_a", ast.span())))); - - let (impl_generics, _, _) = expanded_generic.split_for_impl(); - - // Implementing ValidateArgs - let impl_ast = quote!( - #validate_trait_impl - - // We need this here to prevent formatting lints that can be caused by `quote_spanned!` - // See: rust-lang/rust-clippy#6249 for more reference - #[allow(clippy::all)] - // Triggers when single_use_lifetimes rustc lint is configured in user project and there are no - // usages of 'v_a lifetime in the generated impl definition - #[allow(single_use_lifetimes)] - impl #impl_generics ::validator::ValidateArgs<'v_a> for #ident #ty_generics #where_clause { - type Args = #arg_type; - - #[allow(unused_mut)] - #[allow(unused_variable)] - fn validate_args(&self, args: Self::Args) -> ::std::result::Result<(), ::validator::ValidationErrors> { - let mut errors = ::validator::ValidationErrors::new(); + // Email validation + let email = if let Some(email) = self.email.clone() { + email_tokens( + match email { + Override::Inherit => Email::default(), + Override::Explicit(e) => e, + }, + &field_name, + &field_name_str, + ) + } else { + quote!() + }; - #(#validations)* + // Credit card validation + let card = if let Some(credit_card) = self.credit_card.clone() { + credit_card_tokens( + match credit_card { + Override::Inherit => Card::default(), + Override::Explicit(c) => c, + }, + &field_name, + &field_name_str, + ) + } else { + quote!() + }; - #(#schema_validations)* + // Url validation + let url = if let Some(url) = self.url.clone() { + url_tokens( + match url { + Override::Inherit => Url::default(), + Override::Explicit(u) => u, + }, + &field_name, + &field_name_str, + ) + } else { + quote!() + }; - let mut result = if errors.is_empty() { - ::std::result::Result::Ok(()) - } else { - ::std::result::Result::Err(errors) - }; + // Ip address validation + let ip = if let Some(ip) = self.ip.clone() { + ip_tokens( + match ip { + Override::Inherit => Ip::default(), + Override::Explicit(i) => i, + }, + &field_name, + &field_name_str, + ) + } else { + quote!() + }; - #(#nested_validations)* - result - } - } - ); + // Non control character validation + let ncc = if let Some(ncc) = self.non_control_character.clone() { + non_control_char_tokens( + match ncc { + Override::Inherit => NonControlCharacter::default(), + Override::Explicit(n) => n, + }, + &field_name, + &field_name_str, + ) + } else { + quote!() + }; - // println!("{}", impl_ast.to_string()); + // Range validation + let range = if let Some(range) = self.range.clone() { + range_tokens(range, &field_name, &field_name_str) + } else { + quote!() + }; - impl_ast -} + // Required validation + let required = if let Some(required) = self.required.clone() { + required_tokens( + match required { + Override::Inherit => Required::default(), + Override::Explicit(r) => r, + }, + &field_name, + &field_name_str, + ) + } else { + quote!() + }; -fn collect_fields(ast: &syn::DeriveInput) -> Vec { - match ast.data { - syn::Data::Struct(syn::DataStruct { ref fields, .. }) => { - if fields.iter().any(|field| field.ident.is_none()) { - abort!( - fields.span(), - "struct has unnamed fields"; - help = "#[derive(Validate)] can only be used on structs with named fields"; - ); - } - fields.iter().cloned().collect::>() - } - _ => abort!(ast.span(), "#[derive(Validate)] can only be used with structs"), - } -} + // Required nested validation + let required_nested = if let Some(required_nested) = self.required_nested.clone() { + required_nested_tokens( + match required_nested { + Override::Inherit => Required::default(), + Override::Explicit(r) => r, + }, + &field_name, + &field_name_str, + ) + } else { + quote!() + }; -fn collect_field_validations(ast: &syn::DeriveInput) -> Vec { - let mut fields = collect_fields(ast); - - let field_types = find_fields_type(&fields); - fields.drain(..).fold(vec![], |mut acc, field| { - let key = field.ident.clone().unwrap().to_string(); - let (name, validations) = find_validators_for_field(&field, &field_types); - acc.push(FieldInformation::new( - field, - field_types.get(&key).unwrap().clone(), - name, - validations, - )); - acc - }) -} + // Contains validation + let contains = if let Some(contains) = self.contains.clone() { + contains_tokens(contains, &field_name, &field_name_str) + } else { + quote!() + }; -fn construct_validator_argument_type( - fields_validations: &mut [FieldInformation], - struct_validations: &mut [SchemaValidation], -) -> (proc_macro2::TokenStream, bool) { - const ARGS_PARAMETER_NAME: &str = "args"; - - // This iterator only holds custom validations with a argument_type - let mut customs: Vec<&mut CustomArgument> = fields_validations - .iter_mut() - .flat_map(|x| { - x.validations.iter_mut().filter_map(|x| x.validator.get_custom_argument_mut()) - }) - .collect(); + // 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) + } else { + quote!() + }; - let mut schemas: Vec<&mut CustomArgument> = - struct_validations.iter_mut().filter_map(|x| x.args.as_mut()).collect(); + // 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) + } else { + quote!() + }; - customs.append(&mut schemas); + // Regex validation + let regex = if let Some(regex) = self.regex.clone() { + regex_tokens(regex, &field_name, &field_name_str) + } else { + quote!() + }; - if customs.is_empty() { - // Just the default empty type if no types are defined - (quote!(()), false) - } else if customs.len() == 1 { - // A single parameter will not be wrapped in a tuple - let arg = customs.pop().unwrap(); - arg.arg_access = Some(syn::parse_str(ARGS_PARAMETER_NAME).unwrap()); + // Custom validation + let custom = if let Some(custom) = self.custom.clone() { + custom_tokens(custom, &field_name, &field_name_str) + } else { + quote!() + }; - let type_stream: &Type = &arg.arg_type; - let span = arg.def_span; - (quote_spanned!(span=> #type_stream), true) - } else { - // Multiple times will be wrapped in a tuple - let mut index = 0; - let params = customs.iter_mut().fold(quote!(), |acc, arg| { - let arg_access_string = format!("{}.{}", ARGS_PARAMETER_NAME, index); - arg.arg_access = Some(syn::parse_str(arg_access_string.as_str()).unwrap()); - index += 1; - - let type_stream: &Type = &arg.arg_type; - let span = arg.def_span; - - if index == 1 { - quote_spanned!(span=> #type_stream) + let nested = if let Some(n) = self.nested { + if n { + nested_tokens(&field_name, &field_name_str) } else { - quote_spanned!(span=> #acc, #type_stream) + quote!() } - }); + } else { + quote!() + }; - (quote!((#params)), true) + tokens.extend(quote! { + #length + #email + #card + #url + #ip + #ncc + #range + #required + #required_nested + #contains + #does_not_contain + #must_match + #regex + #custom + #nested + }); } } -fn quote_field_validations( - mut fields: Vec, -) -> (Vec, Vec) { - let mut validations = vec![]; - let mut nested_validations = vec![]; - - fields.drain(..).for_each(|x| { - let field_ident = x.field.ident.clone().unwrap(); - let field_quoter = FieldQuoter::new(field_ident, x.name, x.field_type); - - for validation in &x.validations { - quote_validator(&field_quoter, validation, &mut validations, &mut nested_validations); - } - }); - - (validations, nested_validations) +// The main struct we get from parsing the attributes +// The "supports(struct_named)" attribute guarantees only named structs to work with this macro +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(validate), supports(struct_named))] +#[darling(and_then = ValidationData::validate)] +struct ValidationData { + ident: syn::Ident, + generics: syn::Generics, + data: Data<(), WithOriginal>, + #[darling(multiple)] + schema: Vec, + context: Option, + mutable: Option, + nested: Option, + nest_all_fields: Option, } -/// Find if a struct has some schema validation and returns the info if so -fn find_struct_validation(attr: &syn::Attribute) -> SchemaValidation { - let error = |span: Span, msg: &str| -> ! { - abort!(span, "Invalid schema level validation: {}", msg); - }; - - if_chain! { - if let Ok(syn::Meta::List(syn::MetaList { ref nested, .. })) = attr.parse_meta(); - if let syn::NestedMeta::Meta(syn::Meta::List(syn::MetaList { ref path, ref nested, .. })) = nested[0]; - - then { - let ident = path.get_ident().unwrap(); - if ident != "schema" { - error(attr.span(), "Only `schema` is allowed as validator on a struct") - } - - let mut function = String::new(); - let mut skip_on_field_errors = true; - let mut code = None; - let mut message = None; - let mut args = None; - - for arg in nested { - if_chain! { - if let syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue { ref path, ref lit, .. })) = *arg; - - then { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "function" => { - function = match lit_to_string(lit) { - Some(s) => s, - None => error(lit.span(), "invalid argument type for `function` \ - : only a string is allowed"), - }; - }, - "skip_on_field_errors" => { - skip_on_field_errors = match lit_to_bool(lit) { - Some(s) => s, - None => error(lit.span(), "invalid argument type for `skip_on_field_errors` \ - : only a bool is allowed"), - }; - }, - "code" => { - code = match lit_to_string(lit) { - Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `code` \ - : only a string is allowed"), - }; - }, - "message" => { - message = match lit_to_string(lit) { - Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `message` \ - : only a string is allowed"), - }; - }, - "arg" => { - match lit_to_string(lit) { - Some(s) => { - match syn::parse_str::(s.as_str()) { - Ok(arg_type) => { - assert_custom_arg_type(&lit.span(), &arg_type); - args = Some(CustomArgument::new(lit.span(), arg_type)); - } - Err(_) => { - let mut msg = "invalid argument type for `arg` of `schema` validator: The string has to be a single type.".to_string(); - msg.push_str("\n(Tip: You can combine multiple types into one tuple.)"); - error(lit.span(), msg.as_str()); - } +impl ValidationData { + fn validate(self) -> darling::Result { + if let Some(context) = &self.context { + // Check if context lifetime is not `'v_a` + for segment in &context.segments { + match &segment.arguments { + PathArguments::AngleBracketed(args) => { + for arg in &args.args { + match arg { + syn::GenericArgument::Lifetime(lt) => { + if lt.ident != "v_a" { + abort! { + lt.ident, "Invalid argument reference"; + note = "The lifetime `'{}` is not supported.", lt.ident; + help = "Please use the validator lifetime `'v_a`"; } } - None => error(lit.span(), "invalid argument type for `arg` of `custom` validator: expected a string") - }; - }, - _ => error(lit.span(), "Unknown argument") + } + _ => (), + } } - } else { - error(arg.span(), "Unexpected args") } + _ => (), } } + } - if function.is_empty() { - error(path.span(), "`function` is required"); - } - - SchemaValidation { - function, - args, - skip_on_field_errors, - code, - message, + match &self.data { + Data::Struct(fields) => { + let original_fields: Vec<&Field> = + fields.fields.iter().map(|f| &f.original).collect(); + for f in &fields.fields { + f.parsed.validate(&self.ident, &original_fields, &f.original); + } } - } else { - error(attr.span(), "Unexpected struct validator") + _ => (), } + + Ok(self) } } -/// Finds all struct schema validations -fn find_struct_validations(struct_attrs: &[syn::Attribute]) -> Vec { - struct_attrs - .iter() - .filter(|attribute| attribute.path == parse_quote!(validate)) - .map(find_struct_validation) - .collect() -} +#[proc_macro_error] +#[proc_macro_derive(Validate, attributes(validate))] +pub fn derive_validation(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input: DeriveInput = parse_macro_input!(input); -/// Find the types (as string) for each field of the struct -/// Needed for the `must_match` filter -fn find_fields_type(fields: &[syn::Field]) -> HashMap { - let mut types = HashMap::new(); - - for field in fields { - let field_ident = field.ident.clone().unwrap().to_string(); - let field_type = match field.ty { - syn::Type::Path(syn::TypePath { ref path, .. }) => { - let mut tokens = proc_macro2::TokenStream::new(); - path.to_tokens(&mut tokens); - tokens.to_string().replace(' ', "") - } - syn::Type::Reference(syn::TypeReference { ref lifetime, ref elem, .. }) => { - let mut tokens = proc_macro2::TokenStream::new(); - elem.to_tokens(&mut tokens); - let mut name = tokens.to_string().replace(' ', ""); - if lifetime.is_some() { - name.insert(0, '&') - } - name - } - syn::Type::Group(syn::TypeGroup { ref elem, .. }) => { - let mut tokens = proc_macro2::TokenStream::new(); - elem.to_tokens(&mut tokens); - tokens.to_string().replace(' ', "") - } - _ => { - let mut field_type = proc_macro2::TokenStream::new(); - field.ty.to_tokens(&mut field_type); - field_type.to_string().replace(' ', "") + // parse the input to the ValidationData struct defined above + let validation_data = match ValidationData::from_derive_input(&input) { + Ok(data) => data, + Err(e) => return e.write_errors().into(), + }; + + let custom_context = if let Some(context) = &validation_data.context { + if let Some(mutable) = validation_data.mutable { + if mutable { + quote!(&'v_a mut #context) + } else { + quote!(&'v_a #context) } - }; + } else { + quote!(&'v_a #context) + } + } else { + quote!(()) + }; - //println!("{:?}", field_type); - types.insert(field_ident, field_type); - } + // get all the fields to quote them below + let mut validation_fields: Vec = validation_data + .data + .take_struct() + .unwrap() + .fields + .into_iter() + .map(|f| f.parsed) + // skip fields with #[validate(skip)] attribute + .filter(|f| if let Some(s) = f.skip { !s } else { true }) + .collect(); - types -} + if let Some(nest_all_fields) = validation_data.nest_all_fields { + if nest_all_fields { + validation_fields = validation_fields + .iter_mut() + .map(|f| { + f.nested = Some(true); + f.to_owned() + }) + .collect(); + } + } -/// Find everything we need to know about a field: its real name if it's changed from the serialization -/// and the list of validators to run on it -fn find_validators_for_field( - field: &syn::Field, - field_types: &HashMap, -) -> (String, Vec) { - let rust_ident = field.ident.clone().unwrap().to_string(); - let mut field_ident = field.ident.clone().unwrap().to_string(); - - let error = |span: Span, msg: &str| -> ! { - abort!( - span, - "Invalid attribute #[validate] on field `{}`: {}", - field.ident.clone().unwrap().to_string(), - msg - ); - }; + // generate `use` statements for all used validator traits + let use_statements = quote_use_stmts(&validation_fields); - let field_type = field_types.get(&field_ident).unwrap(); + // Schema validation + let schema = validation_data.schema.iter().fold(quote!(), |acc, s| { + let st = schema_tokens(s.clone()); + let acc = quote! { + #acc + #st + }; + acc + }); - let mut validators = vec![]; - let mut has_validate = false; + let ident = validation_data.ident; + let (imp, ty, whr) = validation_data.generics.split_for_impl(); - for attr in &field.attrs { - if attr.path != parse_quote!(validate) && attr.path != parse_quote!(serde) { - continue; - } + let struct_generics_quote = + validation_data.generics.params.iter().fold(quote!(), |mut q, g| { + q.extend(quote!(#g, )); + q + }); - if attr.path == parse_quote!(validate) { - has_validate = true; - } + let imp_args = if struct_generics_quote.is_empty() { + quote!(<'v_a>) + } else { + quote!(<'v_a, #struct_generics_quote>) + }; - match attr.parse_meta() { - Ok(syn::Meta::List(syn::MetaList { ref nested, .. })) => { - let meta_items = nested.iter().collect::>(); - // original name before serde rename - if attr.path == parse_quote!(serde) { - if let Some(s) = find_original_field_name(&meta_items) { - field_ident = s; + let nested_validation = if validation_data.nested.is_some_and(|n| n) { + quote! { + impl #imp_args ::validator::ValidateNested<'v_a> for #ident #ty #whr { + type Args = #custom_context; + fn validate_nested(&self, field_name: &'static str, args: Self::Args) -> ::std::result::Result<(), ::validator::ValidationErrors> { + use validator::ValidateArgs; + let res = self.validate_with_args(args); + + if let Err(e) = res { + let new_err = validator::ValidationErrorsKind::Struct(::std::boxed::Box::new(e)); + std::result::Result::Err(validator::ValidationErrors(::std::collections::HashMap::from([(field_name, new_err)]))) + } else { + std::result::Result::Ok(()) } - continue; - } - - // only validation from there on - for meta_item in meta_items { - match *meta_item { - syn::NestedMeta::Meta(ref item) => match *item { - // email, url, credit_card, non_control_character - syn::Meta::Path(ref name) => { - match name.get_ident().unwrap().to_string().as_ref() { - "email" => { - validators.push(FieldValidation::new(Validator::Email)); - } - "url" => { - validators.push(FieldValidation::new(Validator::Url)); - } - #[cfg(feature = "card")] - "credit_card" => { - validators - .push(FieldValidation::new(Validator::CreditCard)); - } - #[cfg(feature = "unic")] - "non_control_character" => { - validators.push(FieldValidation::new( - Validator::NonControlCharacter, - )); - } - "required" => { - validators.push(FieldValidation::new(Validator::Required)); - } - "required_nested" => { - validators.push(FieldValidation::new(Validator::Required)); - validators.push(FieldValidation::new(Validator::Nested)); - } - _ => { - let mut ident = proc_macro2::TokenStream::new(); - name.to_tokens(&mut ident); - abort!(name.span(), "Unexpected validator: {}", ident) - } - } - } - // custom, contains, must_match, regex - syn::Meta::NameValue(syn::MetaNameValue { - ref path, ref lit, .. - }) => { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "custom" => { - match lit_to_string(lit) { - Some(s) => validators.push(FieldValidation::new(Validator::Custom { - function: s, - argument: Box::new(None), - })), - None => error(lit.span(), "invalid argument for `custom` validator: only strings are allowed"), - }; - } - "contains" => { - match lit_to_string(lit) { - Some(s) => validators.push(FieldValidation::new(Validator::Contains(s))), - None => error(lit.span(), "invalid argument for `contains` validator: only strings are allowed"), - }; - } - "does_not_contain" => { - match lit_to_string(lit) { - Some(s) => validators.push(FieldValidation::new(Validator::DoesNotContain(s))), - None => error(lit.span(), "invalid argument for `does_not_contain` validator: only strings are allowed"), - }; - } - "regex" => { - match lit_to_string(lit) { - Some(s) => validators.push(FieldValidation::new(Validator::Regex(s))), - None => error(lit.span(), "invalid argument for `regex` validator: only strings are allowed"), - }; - } - "must_match" => { - match lit_to_string(lit) { - Some(s) => { - assert_type_matches(rust_ident.clone(), field_type, field_types.get(&s), attr); - validators.push(FieldValidation::new(Validator::MustMatch(s))); - } - None => error(lit.span(), "invalid argument for `must_match` validator: only strings are allowed"), - }; - } - v => abort!( - path.span(), - "unexpected name value validator: {:?}", - v - ), - }; - } - // Validators with several args - syn::Meta::List(syn::MetaList { ref path, ref nested, .. }) => { - let meta_items = nested.iter().cloned().collect::>(); - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "length" => { - assert_has_len(rust_ident.clone(), field_type, &field.ty); - validators.push(extract_length_validation( - rust_ident.clone(), - attr, - &meta_items, - )); - } - "range" => { - assert_has_range(rust_ident.clone(), field_type, &field.ty); - validators.push(extract_range_validation( - rust_ident.clone(), - attr, - &meta_items, - )); - } - "custom" => { - validators.push(extract_custom_validation( - rust_ident.clone(), - attr, - &meta_items, - )); - } - "email" - | "url" - | "credit_card" - | "non_control_character" - | "required" => { - validators.push(extract_argless_validation( - ident.to_string(), - rust_ident.clone(), - &meta_items, - )); - } - "contains" | "does_not_contain" => { - validators.push(extract_one_arg_validation( - "pattern", - ident.to_string(), - rust_ident.clone(), - &meta_items, - )); - } - "regex" => { - validators.push(extract_one_arg_validation( - "path", - ident.to_string(), - rust_ident.clone(), - &meta_items, - )); - } - "must_match" => { - let validation = extract_one_arg_validation( - "other", - ident.to_string(), - rust_ident.clone(), - &meta_items, - ); - if let Validator::MustMatch(ref t2) = validation.validator { - assert_type_matches( - rust_ident.clone(), - field_type, - field_types.get(t2), - attr, - ); - } - validators.push(validation); - } - v => abort!(path.span(), "unexpected list validator: {:?}", v), - } - } - }, - _ => unreachable!("Found a non Meta while looking for validators"), - }; } } - Ok(syn::Meta::Path(_)) => validators.push(FieldValidation::new(Validator::Nested)), - Ok(syn::Meta::NameValue(_)) => abort!(attr.span(), "Unexpected name=value argument"), - Err(e) => { - let error_string = format!("{:?}", e); - if error_string == "Error(\"expected literal\")" { - abort!(attr.span(), - "This attributes for the field `{}` seem to be misformed, please validate the syntax with the documentation", - field_ident - ); - } else { - abort!( - attr.span(), - "Unable to parse this attribute for the field `{}` with the error: {:?}", - field_ident, - e - ); + } + } else { + quote!() + }; + + let argless_validation = if validation_data.context.is_none() { + quote! { + impl #imp ::validator::Validate for #ident #ty #whr { + fn validate(&self) -> Result<(), ::validator::ValidationErrors> { + use validator::ValidateArgs; + self.validate_with_args(()) } } } + } else { + quote!() + }; - if has_validate && validators.is_empty() { - error(attr.span(), "it needs at least one validator"); - } - } + quote!( + #argless_validation - (field_ident, validators) -} + #nested_validation -/// Serde can be used to rename fields on deserialization but most of the times -/// we want the error on the original field. -/// -/// For example a JS frontend might send camelCase fields and Rust converts them to snake_case -/// but we want to send the errors back with the original name -fn find_original_field_name(meta_items: &[&syn::NestedMeta]) -> Option { - let mut original_name = None; - - for meta_item in meta_items { - match **meta_item { - syn::NestedMeta::Meta(ref item) => match *item { - syn::Meta::Path(_) => continue, - syn::Meta::NameValue(syn::MetaNameValue { ref path, ref lit, .. }) => { - let ident = path.get_ident().unwrap(); - if ident == "rename" { - original_name = Some(lit_to_string(lit).unwrap()); - } - } - syn::Meta::List(syn::MetaList { ref nested, .. }) => { - return find_original_field_name(&nested.iter().collect::>()); - } - }, - _ => unreachable!(), - }; + impl #imp_args ::validator::ValidateArgs<'v_a> for #ident #ty #whr { + type Args = #custom_context; - if original_name.is_some() { - return original_name; - } - } + fn validate_with_args(&self, args: Self::Args) + -> ::std::result::Result<(), ::validator::ValidationErrors> + { + #use_statements - original_name + let mut errors = ::validator::ValidationErrors::new(); + + #(#validation_fields)* + + #schema + + if errors.is_empty() { + ::std::result::Result::Ok(()) + } else { + ::std::result::Result::Err(errors) + } + } + } + ) + .into() } diff --git a/validator_derive/src/lit.rs b/validator_derive/src/lit.rs deleted file mode 100644 index fe57eb0e..00000000 --- a/validator_derive/src/lit.rs +++ /dev/null @@ -1,80 +0,0 @@ -use quote::quote; -use validator_types::ValueOrPath; - -pub fn lit_to_string(lit: &syn::Lit) -> Option { - match *lit { - syn::Lit::Str(ref s) => Some(s.value()), - _ => None, - } -} - -pub fn lit_to_int(lit: &syn::Lit) -> Option { - match *lit { - syn::Lit::Int(ref s) => Some(s.base10_parse().unwrap()), - _ => None, - } -} - -pub fn lit_to_float(lit: &syn::Lit) -> Option { - match *lit { - syn::Lit::Float(ref s) => Some(s.base10_parse::().unwrap()), - syn::Lit::Int(ref s) => Some(s.base10_parse::().unwrap()), - _ => None, - } -} - -pub fn lit_to_u64_or_path(lit: &syn::Lit) -> Option> { - let number = lit_to_int(lit); - if let Some(number) = number { - return Some(ValueOrPath::Value(number)); - } - - let path = lit_to_string(lit); - if let Some(path) = path { - return Some(ValueOrPath::Path(path)); - } - - None -} - -pub fn lit_to_f64_or_path(lit: &syn::Lit) -> Option> { - let number = lit_to_float(lit); - if let Some(number) = number { - return Some(ValueOrPath::Value(number)); - } - - let path = lit_to_string(lit); - if let Some(path) = path { - return Some(ValueOrPath::Path(path)); - } - - None -} - -pub fn lit_to_bool(lit: &syn::Lit) -> Option { - match *lit { - syn::Lit::Bool(ref s) => Some(s.value), - _ => None, - } -} - -pub fn option_to_tokens(opt: &Option) -> proc_macro2::TokenStream { - match opt { - Some(ref t) => quote!(::std::option::Option::Some(#t)), - None => quote!(::std::option::Option::None), - } -} - -pub fn value_or_path_to_tokens(value: &ValueOrPath) -> proc_macro2::TokenStream -where - T: quote::ToTokens + std::clone::Clone + std::cmp::PartialEq + std::fmt::Debug, -{ - match value { - ValueOrPath::Value(ref t) => quote!(#t), - ValueOrPath::Path(ref path) => { - // Global space - let ident: syn::Path = syn::parse_str(path).unwrap(); - quote!(#ident) - } - } -} diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs deleted file mode 100644 index 443a5766..00000000 --- a/validator_derive/src/quoting.rs +++ /dev/null @@ -1,619 +0,0 @@ -use if_chain::if_chain; -use proc_macro2::{self, Span}; -use quote::quote; - -use validator_types::{Validator, ValueOrPath}; - -use crate::asserts::{COW_TYPE, NUMBER_TYPES}; -use crate::lit::{option_to_tokens, value_or_path_to_tokens}; -use crate::validation::{FieldValidation, SchemaValidation}; - -/// Pass around all the information needed for creating a validation -#[derive(Debug)] -pub struct FieldQuoter { - ident: syn::Ident, - /// The field name - name: String, - /// The field type - _type: String, -} - -impl FieldQuoter { - pub fn new(ident: syn::Ident, name: String, _type: String) -> FieldQuoter { - FieldQuoter { ident, name, _type } - } - - /// Don't put a & in front a pointer since we are going to pass - /// a reference to the validator - /// Also just use the ident without if it's optional and will go through - /// a if let first - pub fn quote_validator_param(&self) -> proc_macro2::TokenStream { - let ident = &self.ident; - - if self._type.starts_with("Option<") { - quote!(#ident) - } else if COW_TYPE.is_match(self._type.as_ref()) { - quote!(self.#ident.as_ref()) - } else if self._type.starts_with('&') || NUMBER_TYPES.contains(&self._type.as_ref()) { - quote!(self.#ident) - } else { - quote!(&self.#ident) - } - } - - pub fn quote_validator_field(&self) -> proc_macro2::TokenStream { - let ident = &self.ident; - - if self._type.starts_with("Option<") || is_list(&self._type) || is_map(&self._type) { - quote!(#ident) - } else if COW_TYPE.is_match(self._type.as_ref()) { - quote!(self.#ident.as_ref()) - } else { - quote!(self.#ident) - } - } - - pub fn get_optional_validator_param(&self) -> proc_macro2::TokenStream { - let ident = &self.ident; - if self._type.starts_with("Option<&") - || self._type.starts_with("Option proc_macro2::TokenStream { - let field_ident = &self.ident; - let optional_pattern_matched = self.get_optional_validator_param(); - if self._type.starts_with("Option proc_macro2::TokenStream { - let field_ident = &self.ident; - let field_name = &self.name; - - // When we're using an option, we'll have the field unwrapped, so we should not access it - // through `self`. - let prefix = (!self._type.starts_with("Option<")).then(|| quote! { self. }); - - // When iterating over a list, the iterator has Item=T, while a map yields Item=(K, V), and - // we're only interested in V. - let args = if is_list(&self._type) { - quote! { #field_ident } - } else if is_map(&self._type) { - quote! { (_, #field_ident) } - } else { - return tokens; - }; - - quote! { - if !::validator::ValidationErrors::has_error(&result, #field_name) { - let results: Vec<_> = #prefix #field_ident.iter().map(|#args| { - let mut result = ::std::result::Result::Ok(()); - #tokens - result - }).collect(); - result = ::validator::ValidationErrors::merge_all(result, #field_name, results); - } - } - } -} - -fn is_map(_type: &str) -> bool { - if let Some(stripped) = _type.strip_prefix("Option<") { - is_map(stripped) - } else if let Some(stripped) = _type.strip_prefix("&") { - is_map(stripped) - } else { - _type.starts_with("HashMap<") - || _type.starts_with("FxHashMap<") - || _type.starts_with("FnvHashMap<") - || _type.starts_with("BTreeMap<") - || _type.starts_with("IndexMap<") - } -} - -fn is_list(_type: &str) -> bool { - if let Some(stripped) = _type.strip_prefix("&") { - is_list(stripped) - } else if let Some(stripped) = _type.strip_prefix("Option<") { - is_list(stripped) - } else { - _type.starts_with("Vec<") - || _type.starts_with("HashSet<") - || _type.starts_with("BTreeSet<") - || _type.starts_with("IndexSet<") - || _type.starts_with("[") - } -} - -/// Quote an actual end-user error creation automatically -fn quote_error(validation: &FieldValidation) -> proc_macro2::TokenStream { - let code = &validation.code; - let add_message_quoted = if let Some(ref m) = validation.message { - quote!(err.message = Some(::std::borrow::Cow::from(#m));) - } else { - quote!() - }; - - quote!( - let mut err = ::validator::ValidationError::new(#code); - #add_message_quoted - ) -} - -pub fn quote_length_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - if let Validator::Length { min, max, equal } = &validation.validator { - let min_err_param_quoted = if let Some(v) = min { - let v = value_or_path_to_tokens(v); - quote!(err.add_param(::std::borrow::Cow::from("min"), &#v);) - } else { - quote!() - }; - let max_err_param_quoted = if let Some(v) = max { - let v = value_or_path_to_tokens(v); - quote!(err.add_param(::std::borrow::Cow::from("max"), &#v);) - } else { - quote!() - }; - let equal_err_param_quoted = if let Some(v) = equal { - let v = value_or_path_to_tokens(v); - quote!(err.add_param(::std::borrow::Cow::from("equal"), &#v);) - } else { - quote!() - }; - - let min_tokens = option_to_tokens( - &min.clone().as_ref().map(value_or_path_to_tokens).map(|x| quote!(#x as u64)), - ); - let max_tokens = option_to_tokens( - &max.clone().as_ref().map(value_or_path_to_tokens).map(|x| quote!(#x as u64)), - ); - let equal_tokens = option_to_tokens( - &equal.clone().as_ref().map(value_or_path_to_tokens).map(|x| quote!(#x as u64)), - ); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_length( - #validator_param, - #min_tokens, - #max_tokens, - #equal_tokens - ) { - #quoted_error - #min_err_param_quoted - #max_err_param_quoted - #equal_err_param_quoted - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - return field_quoter.wrap_if_option(quoted); - } - - unreachable!() -} - -pub fn quote_range_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let quoted_ident = field_quoter.quote_validator_param(); - - if let Validator::Range { ref min, ref max, ref exclusive_min, ref exclusive_max } = - validation.validator - { - let min_err_param_quoted = err_param_quoted(min, "min"); - let max_err_param_quoted = err_param_quoted(max, "max"); - let exclusive_min_err_param_quoted = err_param_quoted(exclusive_min, "exclusive_min"); - let exclusive_max_err_param_quoted = err_param_quoted(exclusive_max, "exclusive_max"); - - // Can't interpolate None - let min_tokens = generate_tokens(min); - let max_tokens = generate_tokens(max); - let exclusive_min_tokens = generate_tokens(exclusive_min); - let exclusive_max_tokens = generate_tokens(exclusive_max); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_range( - #quoted_ident as f64, - #min_tokens, - #max_tokens, - #exclusive_min_tokens, - #exclusive_max_tokens, - ) { - #quoted_error - #min_err_param_quoted - #max_err_param_quoted - #exclusive_min_err_param_quoted - #exclusive_max_err_param_quoted - err.add_param(::std::borrow::Cow::from("value"), &#quoted_ident); - errors.add(#field_name, err); - } - ); - - return field_quoter.wrap_if_option(quoted); - } - - unreachable!() -} - -fn err_param_quoted(option: &Option>, name: &str) -> proc_macro2::TokenStream -where - T: std::fmt::Debug + std::clone::Clone + std::cmp::PartialEq + quote::ToTokens, -{ - if let Some(v) = option { - let v = value_or_path_to_tokens(v); - quote!(err.add_param(::std::borrow::Cow::from(#name), &#v);) - } else { - quote!() - } -} - -fn generate_tokens(value: &Option>) -> proc_macro2::TokenStream -where - T: std::fmt::Debug + std::clone::Clone + std::cmp::PartialEq + quote::ToTokens, -{ - let tokens = value.clone().map(|x| value_or_path_to_tokens(&x)).map(|x| quote!(#x as f64)); - option_to_tokens(&tokens) -} - -#[cfg(feature = "card")] -pub fn quote_credit_card_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_credit_card(#validator_param) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - field_quoter.wrap_if_option(quoted) -} - -#[cfg(feature = "unic")] -pub fn quote_non_control_character_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_non_control_character(#validator_param) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - field_quoter.wrap_if_option(quoted) -} - -pub fn quote_url_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_url(#validator_param) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - field_quoter.wrap_if_option(quoted) -} - -pub fn quote_email_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_email(#validator_param) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - field_quoter.wrap_if_option(quoted) -} - -pub fn quote_must_match_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let ident = &field_quoter.ident; - let field_name = &field_quoter.name; - - if let Validator::MustMatch(ref other) = validation.validator { - let other_ident = syn::Ident::new(other, Span::call_site()); - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_must_match(&self.#ident, &self.#other_ident) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &self.#ident); - err.add_param(::std::borrow::Cow::from("other"), &self.#other_ident); - errors.add(#field_name, err); - } - ); - - return field_quoter.wrap_if_option(quoted); - } - - unreachable!(); -} - -pub fn quote_custom_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - if let Validator::Custom { function, argument, .. } = &validation.validator { - let fn_ident: syn::Path = syn::parse_str(function).unwrap(); - - let access = if_chain! { - if let Some(argument) = &**argument; - if let Some(access) = &argument.arg_access; - then { - quote!(, #access) - } else { - quote!() - } - }; - - let add_message_quoted = if let Some(ref m) = validation.message { - quote!(err.message = Some(::std::borrow::Cow::from(#m));) - } else { - quote!() - }; - - let quoted = quote!( - match #fn_ident(#validator_param #access) { - ::std::result::Result::Ok(()) => (), - ::std::result::Result::Err(mut err) => { - #add_message_quoted - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - }, - }; - ); - - return field_quoter.wrap_if_option(quoted); - } - - unreachable!(); -} - -pub fn quote_contains_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - if let Validator::Contains(ref needle) = validation.validator { - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_contains(#validator_param, &#needle) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - err.add_param(::std::borrow::Cow::from("needle"), &#needle); - errors.add(#field_name, err); - } - ); - - return field_quoter.wrap_if_option(quoted); - } - - unreachable!(); -} - -pub fn quote_regex_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - if let Validator::Regex(ref re) = validation.validator { - let re_ident: syn::Path = syn::parse_str(re).unwrap(); - let quoted_error = quote_error(validation); - let quoted = quote!( - if !#re_ident.is_match(#validator_param) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - return field_quoter.wrap_if_option(quoted); - } - - unreachable!(); -} - -pub fn quote_nested_validation(field_quoter: &FieldQuoter) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_field = field_quoter.quote_validator_field(); - let quoted = quote!(result = ::validator::ValidationErrors::merge(result, #field_name, #validator_field.validate());); - field_quoter.wrap_if_option(field_quoter.wrap_if_collection(quoted)) -} - -pub fn quote_validator( - field_quoter: &FieldQuoter, - validation: &FieldValidation, - validations: &mut Vec, - nested_validations: &mut Vec, -) { - match validation.validator { - Validator::Length { .. } => { - validations.push(quote_length_validation(field_quoter, validation)) - } - Validator::Range { .. } => { - validations.push(quote_range_validation(field_quoter, validation)) - } - Validator::Email => validations.push(quote_email_validation(field_quoter, validation)), - Validator::Url => validations.push(quote_url_validation(field_quoter, validation)), - Validator::MustMatch(_) => { - validations.push(quote_must_match_validation(field_quoter, validation)) - } - Validator::Custom { .. } => { - validations.push(quote_custom_validation(field_quoter, validation)) - } - Validator::Contains(_) => { - validations.push(quote_contains_validation(field_quoter, validation)) - } - Validator::Regex(_) => validations.push(quote_regex_validation(field_quoter, validation)), - #[cfg(feature = "card")] - Validator::CreditCard => { - validations.push(quote_credit_card_validation(field_quoter, validation)) - } - Validator::Nested => nested_validations.push(quote_nested_validation(field_quoter)), - #[cfg(feature = "unic")] - Validator::NonControlCharacter => { - validations.push(quote_non_control_character_validation(field_quoter, validation)) - } - Validator::Required | Validator::RequiredNested => { - validations.push(quote_required_validation(field_quoter, validation)) - } - Validator::DoesNotContain(_) => { - validations.push(quote_does_not_contain_validation(field_quoter, validation)) - } - } -} - -pub fn quote_schema_validation(v: &SchemaValidation) -> proc_macro2::TokenStream { - let fn_ident: syn::Path = syn::parse_str(&v.function).unwrap(); - - let arg_quoted = if let Some(ref args) = v.args { - let arg_type = &args.arg_access; - quote!(self, #arg_type) - } else { - quote!(self) - }; - - let add_message_quoted = if let Some(ref m) = v.message { - quote!(err.message = Some(::std::borrow::Cow::from(#m));) - } else { - quote!() - }; - - let mut_err_token = if v.message.is_some() { quote!(mut) } else { quote!() }; - - let quoted = quote!( - match #fn_ident(#arg_quoted) { - ::std::result::Result::Ok(()) => (), - ::std::result::Result::Err(#mut_err_token err) => { - #add_message_quoted - errors.add("__all__", err); - }, - }; - ); - - if !v.skip_on_field_errors { - return quoted; - } - - quote!( - #quoted - ) -} - -pub fn quote_schema_validations(validation: &[SchemaValidation]) -> Vec { - validation.iter().map(quote_schema_validation).collect() -} - -pub fn quote_required_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let ident = &field_quoter.ident; - let validator_param = quote!(&self.#ident); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_required(#validator_param) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - quoted -} - -pub fn quote_does_not_contain_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - if let Validator::DoesNotContain(ref needle) = validation.validator { - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_does_not_contain(#validator_param, &#needle) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - err.add_param(::std::borrow::Cow::from("needle"), &#needle); - errors.add(#field_name, err); - } - ); - - return field_quoter.wrap_if_option(quoted); - } - - unreachable!(); -} diff --git a/validator_derive/src/tokens/cards.rs b/validator_derive/src/tokens/cards.rs new file mode 100644 index 00000000..6b9813c8 --- /dev/null +++ b/validator_derive/src/tokens/cards.rs @@ -0,0 +1,23 @@ +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_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() { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/contains.rs b/validator_derive/src/tokens/contains.rs new file mode 100644 index 00000000..f63e89e6 --- /dev/null +++ b/validator_derive/src/tokens/contains.rs @@ -0,0 +1,28 @@ +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_str: &str, +) -> proc_macro2::TokenStream { + let p = contains.pattern; + let (needle, needle_err) = + (quote!(#p), quote!(err.add_param(::std::borrow::Cow::from("needle"), &#p);)); + + let message = quote_message(contains.message); + let code = quote_code(contains.code, "contains"); + + quote! { + if !self.#field_name.validate_contains(#needle) { + #code + #message + #needle_err + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/custom.rs b/validator_derive/src/tokens/custom.rs new file mode 100644 index 00000000..83822718 --- /dev/null +++ b/validator_derive/src/tokens/custom.rs @@ -0,0 +1,45 @@ +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_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) + } else { + quote! (&self.#field_name) + } + } else { + quote!(&self.#field_name) + }; + + let message = quote_message(custom.message); + + let code = if let Some(c) = custom.code { + quote!( + err.code = ::std::borrow::Cow::from(#c); + ) + } else { + quote!() + }; + + quote! { + match #fn_call(#args) { + ::std::result::Result::Ok(()) => {} + ::std::result::Result::Err(mut err) => { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#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 new file mode 100644 index 00000000..79b81847 --- /dev/null +++ b/validator_derive/src/tokens/does_not_contain.rs @@ -0,0 +1,29 @@ +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_str: &str, +) -> proc_macro2::TokenStream { + let p = does_not_contain.pattern; + + let (needle, needle_err) = + (quote!(#p), quote!(err.add_param(::std::borrow::Cow::from("needle"), &#p);)); + + let message = quote_message(does_not_contain.message); + let code = quote_code(does_not_contain.code, "does_not_contain"); + + quote! { + if !self.#field_name.validate_does_not_contain(#needle) { + #code + #message + #needle_err + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/email.rs b/validator_derive/src/tokens/email.rs new file mode 100644 index 00000000..c42f4020 --- /dev/null +++ b/validator_derive/src/tokens/email.rs @@ -0,0 +1,23 @@ +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_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() { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/ip.rs b/validator_derive/src/tokens/ip.rs new file mode 100644 index 00000000..3dbaf283 --- /dev/null +++ b/validator_derive/src/tokens/ip.rs @@ -0,0 +1,42 @@ +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 { + let message = quote_message(ip.message); + let code = quote_code(ip.code, "ip"); + + let version = match (ip.v4, ip.v6) { + (Some(v4), Some(v6)) => match (v4, v6) { + (true, false) => quote!(validate_ipv4()), + (false, true) => quote!(validate_ipv6()), + _ => quote!(validate_ip()), + }, + (Some(v4), None) => { + if v4 { + quote!(validate_ipv4()) + } else { + quote!(validate_ip()) + } + } + (None, Some(v6)) => { + if v6 { + quote!(validate_ipv6()) + } else { + quote!(validate_ip()) + } + } + _ => quote!(validate_ip()), + }; + + quote! { + if !self.#field_name.#version { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/length.rs b/validator_derive/src/tokens/length.rs new file mode 100644 index 00000000..d0065a08 --- /dev/null +++ b/validator_derive/src/tokens/length.rs @@ -0,0 +1,42 @@ +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_str: &str, +) -> proc_macro2::TokenStream { + let (min, min_err) = if let Some(v) = length.min.as_ref() { + (quote!(Some(#v)), quote!(err.add_param(::std::borrow::Cow::from("min"), &#v);)) + } else { + (quote!(None), quote!()) + }; + let (max, max_err) = if let Some(v) = length.max { + (quote!(Some(#v)), quote!(err.add_param(::std::borrow::Cow::from("max"), &#v);)) + } else { + (quote!(None), quote!()) + }; + let (equal, equal_err) = if let Some(v) = length.equal { + (quote!(Some(#v)), quote!(err.add_param(::std::borrow::Cow::from("equal"), &#v);)) + } else { + (quote!(None), quote!()) + }; + + let message = quote_message(length.message); + let code = quote_code(length.code, "length"); + + quote! { + if !self.#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); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/mod.rs b/validator_derive/src/tokens/mod.rs new file mode 100644 index 00000000..31d64f19 --- /dev/null +++ b/validator_derive/src/tokens/mod.rs @@ -0,0 +1,16 @@ +pub mod cards; +pub mod contains; +pub mod custom; +pub mod does_not_contain; +pub mod email; +pub mod ip; +pub mod length; +pub mod must_match; +pub mod nested; +pub mod non_control_character; +pub mod range; +pub mod regex; +pub mod required; +pub mod required_nested; +pub mod schema; +pub mod url; diff --git a/validator_derive/src/tokens/must_match.rs b/validator_derive/src/tokens/must_match.rs new file mode 100644 index 00000000..1819904d --- /dev/null +++ b/validator_derive/src/tokens/must_match.rs @@ -0,0 +1,28 @@ +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_str: &str, +) -> proc_macro2::TokenStream { + let o = must_match.other; + let (other, other_err) = + (quote!(self.#o), quote!(err.add_param(::std::borrow::Cow::from("other"), &self.#o);)); + + let message = quote_message(must_match.message); + let code = quote_code(must_match.code, "must_match"); + + quote! { + if !::validator::validate_must_match(&self.#field_name, &#other) { + #code + #message + #other_err + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/nested.rs b/validator_derive/src/tokens/nested.rs new file mode 100644 index 00000000..979ca7d9 --- /dev/null +++ b/validator_derive/src/tokens/nested.rs @@ -0,0 +1,10 @@ +use quote::quote; +use syn::Ident; + +pub fn nested_tokens(field_name: &Ident, field_name_str: &str) -> proc_macro2::TokenStream { + quote! { + if !errors.0.contains_key(#field_name_str) { + errors.merge_self(#field_name_str, self.#field_name.validate_nested(#field_name_str, args)); + } + } +} diff --git a/validator_derive/src/tokens/non_control_character.rs b/validator_derive/src/tokens/non_control_character.rs new file mode 100644 index 00000000..4c0b9982 --- /dev/null +++ b/validator_derive/src/tokens/non_control_character.rs @@ -0,0 +1,23 @@ +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_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() { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/range.rs b/validator_derive/src/tokens/range.rs new file mode 100644 index 00000000..7a0b0b4a --- /dev/null +++ b/validator_derive/src/tokens/range.rs @@ -0,0 +1,51 @@ +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_str: &str, +) -> proc_macro2::TokenStream { + let (min, min_err) = if let Some(m) = range.min { + (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("min"), &#m);)) + } else { + (quote!(None), quote!()) + }; + + let (max, max_err) = if let Some(m) = range.max { + (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("max"), &#m);)) + } else { + (quote!(None), quote!()) + }; + + let (ex_min, ex_min_err) = if let Some(m) = range.exclusive_min { + (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("exclusive_min"), &#m);)) + } else { + (quote!(None), quote!()) + }; + + let (ex_max, ex_max_err) = if let Some(m) = range.exclusive_max { + (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("exclusive_max"), &#m);)) + } else { + (quote!(None), quote!()) + }; + + let message = quote_message(range.message); + let code = quote_code(range.code, "range"); + + quote! { + if !self.#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); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/regex.rs b/validator_derive/src/tokens/regex.rs new file mode 100644 index 00000000..fe3d1ca3 --- /dev/null +++ b/validator_derive/src/tokens/regex.rs @@ -0,0 +1,24 @@ +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_str: &str, +) -> proc_macro2::TokenStream { + let path = regex.path; + let message = quote_message(regex.message); + let code = quote_code(regex.code, "regex"); + + quote! { + if !&self.#field_name.validate_regex(&#path) { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/tokens/required.rs b/validator_derive/src/tokens/required.rs new file mode 100644 index 00000000..22926e02 --- /dev/null +++ b/validator_derive/src/tokens/required.rs @@ -0,0 +1,23 @@ +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_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() { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#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 new file mode 100644 index 00000000..8d57c78e --- /dev/null +++ b/validator_derive/src/tokens/required_nested.rs @@ -0,0 +1,27 @@ +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_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() { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + + if let Some(ref #field_name) = self.#field_name { + errors.merge_self(#field_name_str, #field_name.validate()); + } + } +} diff --git a/validator_derive/src/tokens/schema.rs b/validator_derive/src/tokens/schema.rs new file mode 100644 index 00000000..afead3cd --- /dev/null +++ b/validator_derive/src/tokens/schema.rs @@ -0,0 +1,52 @@ +use quote::quote; + +use crate::types::Schema; +use crate::utils::quote_message; + +pub fn schema_tokens(schema: Schema) -> proc_macro2::TokenStream { + let fn_call = schema.function; + let args = if let Some(args) = schema.use_context { + if args { + quote!(&self, args) + } else { + quote!(&self) + } + } else { + quote!(&self) + }; + + let skip_on_errors = schema.skip_on_field_errors.unwrap_or(true); + + let message = quote_message(schema.message); + + let code = if let Some(c) = schema.code { + quote!( + err.code = ::std::borrow::Cow::from(#c); + ) + } else { + quote!() + }; + + let fn_call = quote! { + match #fn_call(#args) { + ::std::result::Result::Ok(()) => {} + ::std::result::Result::Err(mut err) => { + #code + #message + errors.add("__all__", err); + } + } + }; + + if skip_on_errors { + quote! { + if errors.is_empty() || ((errors.field_errors().len() == 1) && errors.field_errors().contains_key("__all__")) { + #fn_call + } + } + } else { + quote! { + #fn_call + } + } +} diff --git a/validator_derive/src/tokens/url.rs b/validator_derive/src/tokens/url.rs new file mode 100644 index 00000000..4c5d9d6e --- /dev/null +++ b/validator_derive/src/tokens/url.rs @@ -0,0 +1,19 @@ +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 { + let message = quote_message(url.message); + let code = quote_code(url.code, "url"); + + quote! { + if !self.#field_name.validate_url() { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/types.rs b/validator_derive/src/types.rs new file mode 100644 index 00000000..93fac35b --- /dev/null +++ b/validator_derive/src/types.rs @@ -0,0 +1,214 @@ +use darling::util::Override; +use darling::{FromField, FromMeta}; + +use proc_macro_error::abort; +use syn::spanned::Spanned; +use syn::{Expr, Field, Ident, Path}; + +use crate::utils::get_attr; + +// 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 { + pub ident: Option, + pub ty: syn::Type, + pub credit_card: Option>, + pub contains: Option, + pub does_not_contain: Option, + pub email: Option>, + pub ip: Option>, + pub length: Option, + pub must_match: Option, + pub non_control_character: Option>, + pub range: Option, + pub required: Option>, + pub required_nested: Option>, + pub url: Option>, + pub regex: Option, + pub custom: Option, + pub skip: Option, + pub nested: Option, +} + +impl ValidateField { + pub fn validate(&self, struct_ident: &Ident, all_fields: &Vec<&Field>, current_field: &Field) { + let field_name = self.ident.clone().expect("Field is not a named field").to_string(); + let field_attrs = ¤t_field.attrs; + + if let Some(custom) = &self.custom { + // If function is not a path + if let Err(e) = &custom.function { + abort!( + e.span(), "Invalid attribute #[validate(custom(...))] on field `{}`:", field_name; + note = "Invalid argument for `custom` validator, only paths are allowed"; + help = "Try formating the argument like `path::to::function` or `\"path::to::function\"`" + ); + } + } + + if let Some(length) = &self.length { + // If length has both `equal` and `min` or `max` argument + if length.equal.is_some() && (length.min.is_some() || length.max.is_some()) { + abort! { + length.equal.clone().unwrap().span(), "Invalid attribute #[validate(length(...))] on field `{}`:", field_name; + note = "Both `equal` and `min` or `max` have been set"; + help = "Exclusively use either the `equal` or `min` and `max` attributes" + } + } + + // Check if validator has no arguments + if length.equal.is_none() && length.min.is_none() && length.max.is_none() { + abort!( + get_attr(field_attrs, "length").unwrap(), "Invalid attribute #[validate(length(...))] on field `{}`:", field_name; + note = "Validator `length` requires at least 1 argument"; + help = "Add the argument `equal`, `min` or `max`" + ) + } + } + + if let Some(must_match) = &self.must_match { + let other_field = must_match + .other + .get_ident() + .expect("Cannot get ident from `other` field value") + .to_string(); + + // Check if the other field exists + if !all_fields.iter().any(|f| f.ident.clone().unwrap() == other_field) { + abort!( + must_match.other.span(), "Invalid attribute for #[validate(must_match(...))] on field `{}`:", field_name; + note = "The `other` field doesn't exist in the struct `{}`", struct_ident; + help = "Add the field `{}` to the struct", other_field + ) + } + } + + if let Some(range) = &self.range { + // Check if validator has no arguments + if range.min.is_none() + && range.max.is_none() + && range.exclusive_min.is_none() + && range.exclusive_max.is_none() + { + abort!( + get_attr(field_attrs, "range").unwrap(), "Invalid attribute #[validate(range(...))] on field `{}`:", field_name; + note = "Validator `range` requires at least 1 argument"; + help = "Add the argument `min` or `max`, `exclusive_min` or `exclusive_max`" + ) + } + } + } +} + +// Structs to hold the validation information and to provide attributes +// The name of a field here corresponds to an attribute like +// #[validate(card(message = "something's wrong", code = "1234"))] +// ^^^^^^^ ^^^^ +// +#[derive(Debug, Clone, FromMeta, Default)] +pub struct Card { + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct Contains { + pub pattern: String, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct DoesNotContain { + pub pattern: String, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta, Default)] +pub struct Email { + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta, Default)] +pub struct Ip { + pub v4: Option, + pub v6: Option, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct Length { + pub min: Option, + pub max: Option, + pub equal: Option, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct MustMatch { + pub other: Path, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta, Default)] +pub struct NonControlCharacter { + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct Range { + pub min: Option, + pub max: Option, + pub exclusive_min: Option, + pub exclusive_max: Option, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta, Default)] +pub struct Required { + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta, Default)] +pub struct Url { + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct Regex { + pub path: Expr, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct Custom { + pub function: darling::Result, + pub use_context: Option, + pub message: Option, + pub code: Option, +} + +#[derive(Debug, Clone, FromMeta)] +pub struct Schema { + pub function: Path, + pub use_context: Option, + pub skip_on_field_errors: Option, + pub message: Option, + pub code: Option, +} diff --git a/validator_derive/src/utils.rs b/validator_derive/src/utils.rs new file mode 100644 index 00000000..32f86d34 --- /dev/null +++ b/validator_derive/src/utils.rs @@ -0,0 +1,140 @@ +use quote::quote; +use syn::Attribute; + +use crate::ValidateField; + +pub fn quote_message(message: Option) -> proc_macro2::TokenStream { + if let Some(m) = message { + quote!( + err.message = Some(::std::borrow::Cow::from(#m)); + ) + } else { + quote!() + } +} + +pub fn quote_code(code: Option, default: &str) -> proc_macro2::TokenStream { + if let Some(c) = code { + quote!( + let mut err = ::validator::ValidationError::new(#c); + ) + } else { + quote!( + let mut err = ::validator::ValidationError::new(#default); + ) + } +} + +pub fn quote_use_stmts(fields: &Vec) -> proc_macro2::TokenStream { + let mut length = quote!(); + let mut email = quote!(); + let mut card = quote!(); + let mut url = quote!(); + let mut ip = quote!(); + let mut ncc = quote!(); + let mut range = quote!(); + let mut reqired = quote!(); + let mut contains = quote!(); + let mut does_not_contain = quote!(); + let mut regex = quote!(); + let mut nested = quote!(); + + for f in fields { + if f.length.is_some() { + length = quote!( + use validator::ValidateLength; + ); + } + + if f.email.is_some() { + email = quote!( + use validator::ValidateEmail; + ); + } + + if f.credit_card.is_some() { + card = quote!( + use validator::ValidateCreditCard; + ); + } + + if f.url.is_some() { + url = quote!( + use validator::ValidateUrl; + ); + } + + if f.ip.is_some() { + ip = quote!( + use validator::ValidateIp; + ); + } + + if f.non_control_character.is_some() { + ncc = quote!( + use validator::ValidateNonControlCharacter; + ); + } + + if f.range.is_some() { + range = quote!( + use validator::ValidateRange; + ); + } + + if f.required.is_some() || f.required_nested.is_some() { + reqired = quote!( + use validator::ValidateRequired; + ); + } + + if f.contains.is_some() { + contains = quote!( + use validator::ValidateContains; + ); + } + + if f.does_not_contain.is_some() { + does_not_contain = quote!( + use validator::ValidateDoesNotContain; + ); + } + + if f.regex.is_some() { + regex = quote!( + use validator::ValidateRegex; + ); + } + + if f.nested.is_some() { + nested = quote!( + use validator::ValidateNested; + ); + } + } + + quote!( + #length + #email + #card + #url + #ip + #ncc + #range + #reqired + #contains + #does_not_contain + #regex + #nested + ) +} + +pub fn get_attr<'a>(attrs: &'a Vec, name: &str) -> Option<&'a Attribute> { + attrs.iter().find(|a| match &a.meta { + syn::Meta::List(list) => list.tokens.clone().into_iter().any(|t| match t { + proc_macro2::TokenTree::Ident(i) => &i.to_string() == name, + _ => false, + }), + _ => false, + }) +} diff --git a/validator_derive/src/validation.rs b/validator_derive/src/validation.rs deleted file mode 100644 index 34d768d6..00000000 --- a/validator_derive/src/validation.rs +++ /dev/null @@ -1,466 +0,0 @@ -use proc_macro2::Span; -use proc_macro_error::abort; -use syn::spanned::Spanned; - -use validator_types::{CustomArgument, Validator}; - -use crate::{asserts::assert_custom_arg_type, lit::*}; - -#[derive(Debug)] -pub struct SchemaValidation { - pub function: String, - pub args: Option, - pub skip_on_field_errors: bool, - pub code: Option, - pub message: Option, -} - -/// This struct holds the combined validation information for one filed -#[derive(Debug)] -pub struct FieldInformation { - pub field: syn::Field, - pub field_type: String, - pub name: String, - pub validations: Vec, -} - -impl FieldInformation { - pub fn new( - field: syn::Field, - field_type: String, - name: String, - validations: Vec, - ) -> Self { - FieldInformation { field, field_type, name, validations } - } -} - -/// This struct holds information about one specific validation with it's code, message and validator. -#[derive(Debug)] -pub struct FieldValidation { - pub code: String, - pub message: Option, - pub validator: Validator, -} - -impl FieldValidation { - pub fn new(validator: Validator) -> FieldValidation { - FieldValidation { code: validator.code().to_string(), validator, message: None } - } -} - -pub fn extract_length_validation( - field: String, - attr: &syn::Attribute, - meta_items: &[syn::NestedMeta], -) -> FieldValidation { - let mut min = None; - let mut max = None; - let mut equal = None; - - let (message, code) = extract_message_and_code("length", &field, meta_items); - - let error = |span: Span, msg: &str| -> ! { - abort!(span, "Invalid attribute #[validate] on field `{}`: {}", field, msg); - }; - - for meta_item in meta_items { - if let syn::NestedMeta::Meta(ref item) = *meta_item { - if let syn::Meta::NameValue(syn::MetaNameValue { ref path, ref lit, .. }) = *item { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "message" | "code" => continue, - "min" => { - min = match lit_to_u64_or_path(lit) { - Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `min` of `length` validator: only number literals or value paths are allowed"), - }; - } - "max" => { - max = match lit_to_u64_or_path(lit) { - Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `max` of `length` validator: only number literals or value paths are allowed"), - }; - } - "equal" => { - equal = match lit_to_u64_or_path(lit) { - Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `equal` of `length` validator: only number literals or value paths are allowed"), - }; - } - v => error(path.span(), &format!( - "unknown argument `{}` for validator `length` (it only has `min`, `max`, `equal`)", - v - )) - } - } else { - error( - item.span(), - &format!( - "unexpected item {:?} while parsing `length` validator of field {}", - item, field - ), - ) - } - } - - if equal.is_some() && (min.is_some() || max.is_some()) { - error(meta_item.span(), "both `equal` and `min` or `max` have been set in `length` validator: probably a mistake"); - } - } - - if min.is_none() && max.is_none() && equal.is_none() { - error( - attr.span(), - "Validator `length` requires at least 1 argument out of `min`, `max` and `equal`", - ); - } - - let validator = Validator::Length { min, max, equal }; - FieldValidation { - message, - code: code.unwrap_or_else(|| validator.code().to_string()), - validator, - } -} - -const RANGE_MIN_KEY: &str = "min"; -const RANGE_EXCLUSIVE_MIN_KEY: &str = "exclusive_min"; -const RANGE_MAX_KEY: &str = "max"; -const RANGE_EXCLUSIVE_MAX_KEY: &str = "exclusive_max"; - -pub fn extract_range_validation( - field: String, - attr: &syn::Attribute, - meta_items: &[syn::NestedMeta], -) -> FieldValidation { - let mut min = None; - let mut max = None; - let mut exclusive_min = None; - let mut exclusive_max = None; - - let (message, code) = extract_message_and_code("range", &field, meta_items); - - let error = |span: Span, msg: &str| -> ! { - abort!(span, "Invalid attribute #[validate] on field `{}`: {}", field, msg); - }; - - for meta_item in meta_items { - match *meta_item { - syn::NestedMeta::Meta(ref item) => match *item { - syn::Meta::NameValue(syn::MetaNameValue { ref path, ref lit, .. }) => { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "message" | "code" => continue, - RANGE_MIN_KEY => { - min = match lit_to_f64_or_path(lit) { - Some(s) => Some(s), - None => error(lit.span(), &lit_to_f64_error_message(RANGE_MIN_KEY)) - }; - } - RANGE_EXCLUSIVE_MIN_KEY => { - exclusive_min = match lit_to_f64_or_path(lit) { - Some(s) => Some(s), - None => error(lit.span(), &lit_to_f64_error_message(RANGE_EXCLUSIVE_MIN_KEY)) - }; - } - RANGE_MAX_KEY => { - max = match lit_to_f64_or_path(lit) { - Some(s) => Some(s), - None => error(lit.span(), &lit_to_f64_error_message(RANGE_MAX_KEY)) - }; - } - RANGE_EXCLUSIVE_MAX_KEY => { - exclusive_max = match lit_to_f64_or_path(lit) { - Some(s) => Some(s), - None => error(lit.span(), &lit_to_f64_error_message(RANGE_EXCLUSIVE_MAX_KEY)) - }; - } - v => error(path.span(), &format!( - "unknown argument `{}` for validator `range` (it only has `min`, `max`)", - v - )), - } - } - _ => abort!( - item.span(), - "unexpected item {:?} while parsing `range` validator", - item - ), - }, - _ => unreachable!(), - } - } - - if [&min, &max, &exclusive_min, &exclusive_max].iter().all(|x| x.is_none()) { - error( - attr.span(), - &format!( - "Validator `range` requires at least 1 argument out of `{}`, `{}`, `{}` and `{}`", - RANGE_MIN_KEY, RANGE_MAX_KEY, RANGE_EXCLUSIVE_MIN_KEY, RANGE_EXCLUSIVE_MAX_KEY - ), - ); - } - - if min.is_some() && exclusive_min.is_some() || max.is_some() && exclusive_max.is_some() { - error( - attr.span(), - &format!( - "Validator `range` cannot contain one of its limits (`{}`, `{}`) and its exclusive counterpart", - RANGE_MIN_KEY, RANGE_MAX_KEY - ) - ) - } - let validator = Validator::Range { min, max, exclusive_min, exclusive_max }; - FieldValidation { - message, - code: code.unwrap_or_else(|| validator.code().to_string()), - validator, - } -} - -fn lit_to_f64_error_message(val_name: &str) -> String { - format!("invalid argument type for `{}` of `range` validator: only number literals or value paths are allowed", val_name) -} - -pub fn extract_custom_validation( - field: String, - attr: &syn::Attribute, - meta_items: &[syn::NestedMeta], -) -> FieldValidation { - let mut function = None; - let mut argument = None; - - let (message, code) = extract_message_and_code("custom", &field, meta_items); - - let error = |span: Span, msg: &str| -> ! { - abort!(span, "Invalid attribute #[validate] on field `{}`: {}", field, msg); - }; - - for meta_item in meta_items { - match *meta_item { - syn::NestedMeta::Meta(ref item) => match *item { - syn::Meta::NameValue(syn::MetaNameValue { ref path, ref lit, .. }) => { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "message" | "code" => continue, - "function" => { - function = match lit_to_string(lit) { - Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `function` of `custom` validator: expected a string") - }; - } - "arg" => { - match lit_to_string(lit) { - Some(s) => { - match syn::parse_str::(s.as_str()) { - Ok(arg_type) => { - assert_custom_arg_type(&lit.span(), &arg_type); - argument = Some(CustomArgument::new(lit.span(), arg_type)); - } - Err(_) => { - let mut msg = "invalid argument type for `arg` of `custom` validator: The string has to be a single type.".to_string(); - msg.push_str("\n(Tip: You can combine multiple types into one tuple.)"); - - error(lit.span(), msg.as_str()); - } - } - } - None => error(lit.span(), "invalid argument type for `arg` of `custom` validator: expected a string") - }; - } - v => error(path.span(), &format!( - "unknown argument `{}` for validator `custom` (it only has `function`, `arg`)", - v - )), - } - } - _ => abort!( - item.span(), - "unexpected item {:?} while parsing `custom` validator", - item - ), - }, - _ => unreachable!(), - } - } - - if function.is_none() { - error(attr.span(), "The validator `custom` requires the `function` parameter."); - } - - let validator = Validator::Custom { function: function.unwrap(), argument: Box::new(argument) }; - FieldValidation { - message, - code: code.unwrap_or_else(|| validator.code().to_string()), - validator, - } -} - -/// Extract url/email/non_control_character field validation with a code or a message -pub fn extract_argless_validation( - validator_name: String, - field: String, - meta_items: &[syn::NestedMeta], -) -> FieldValidation { - let (message, code) = extract_message_and_code(&validator_name, &field, meta_items); - - for meta_item in meta_items { - match *meta_item { - syn::NestedMeta::Meta(ref item) => match *item { - syn::Meta::NameValue(syn::MetaNameValue { ref path, .. }) => { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "message" | "code" => continue, - v => abort!( - meta_item.span(), - "Unknown argument `{}` for validator `{}` on field `{}`", - v, - validator_name, - field - ), - } - } - _ => abort!( - meta_item.span(), - "unexpected item {:?} while parsing `range` validator", - item - ), - }, - _ => unreachable!(), - } - } - - let validator = match validator_name.as_ref() { - "email" => Validator::Email, - #[cfg(feature = "card")] - "credit_card" => Validator::CreditCard, - #[cfg(feature = "unic")] - "non_control_character" => Validator::NonControlCharacter, - "required" => Validator::Required, - _ => Validator::Url, - }; - - FieldValidation { - message, - code: code.unwrap_or_else(|| validator.code().to_string()), - validator, - } -} - -/// For custom, contains, regex, must_match -pub fn extract_one_arg_validation( - val_name: &str, - validator_name: String, - field: String, - meta_items: &[syn::NestedMeta], -) -> FieldValidation { - let mut value = None; - let (message, code) = extract_message_and_code(&validator_name, &field, meta_items); - - for meta_item in meta_items { - match *meta_item { - syn::NestedMeta::Meta(ref item) => match *item { - syn::Meta::NameValue(syn::MetaNameValue { ref path, ref lit, .. }) => { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "message" | "code" => continue, - v if v == val_name => { - value = match lit_to_string(lit) { - Some(s) => Some(s), - None => abort!( - item.span(), - "Invalid argument type for `{}` for validator `{}` on field `{}`: only a string is allowed", - val_name, validator_name, field - ), - }; - } - v => abort!( - path.span(), - "Unknown argument `{}` for validator `{}` on field `{}`", - v, - validator_name, - field - ), - } - } - _ => abort!( - item.span(), - "unexpected item {:?} while parsing `range` validator", - item - ), - }, - _ => unreachable!(), - } - - if value.is_none() { - abort!( - meta_item.span(), - "Missing argument `{}` for validator `{}` on field `{}`", - val_name, - validator_name, - field - ); - } - } - - let validator = match validator_name.as_ref() { - "custom" => Validator::Custom { function: value.unwrap(), argument: Box::new(None) }, - "contains" => Validator::Contains(value.unwrap()), - "does_not_contain" => Validator::DoesNotContain(value.unwrap()), - "must_match" => Validator::MustMatch(value.unwrap()), - "regex" => Validator::Regex(value.unwrap()), - _ => unreachable!(), - }; - - FieldValidation { - message, - code: code.unwrap_or_else(|| validator.code().to_string()), - validator, - } -} - -fn extract_message_and_code( - validator_name: &str, - field: &str, - meta_items: &[syn::NestedMeta], -) -> (Option, Option) { - let mut message = None; - let mut code = None; - - for meta_item in meta_items { - if let syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue { - ref path, - ref lit, - .. - })) = *meta_item - { - let ident = path.get_ident().unwrap(); - match ident.to_string().as_ref() { - "code" => { - code = match lit_to_string(lit) { - Some(s) => Some(s), - None => abort!( - meta_item.span(), - "Invalid argument type for `code` for validator `{}` on field `{}`: only a string is allowed", - validator_name, field - ), - }; - } - "message" => { - message = match lit_to_string(lit) { - Some(s) => Some(s), - None => abort!( - meta_item.span(), - "Invalid argument type for `message` for validator `{}` on field `{}`: only a string is allowed", - validator_name, field - ), - }; - } - _ => continue, - } - } - } - - (message, code) -} diff --git a/validator_derive_tests/Cargo.toml b/validator_derive_tests/Cargo.toml index 4e838750..fcde1647 100644 --- a/validator_derive_tests/Cargo.toml +++ b/validator_derive_tests/Cargo.toml @@ -2,10 +2,19 @@ name = "validator_derive_tests" version = "0.1.0" authors = ["Vincent Prouillet "] -edition = "2018" +edition = "2021" + +[features] +default = ["test_ui"] +test_ui = [] [dev-dependencies] -validator = { version = "0.16", path = "../validator", features = ["card", "unic", "derive", "indexmap"] } +validator = { version = "0.16", path = "../validator", features = [ + "card", + "unic", + "derive", + "indexmap", +] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" trybuild = "1.0" @@ -13,4 +22,4 @@ regex = "1" lazy_static = "1" [dependencies] -indexmap = {version = "1", features = ["serde-1"], optional = true } +indexmap = { version = "2", features = ["serde"], optional = true } diff --git a/validator_derive_tests/src/main.rs b/validator_derive_tests/src/main.rs deleted file mode 100644 index e7a11a96..00000000 --- a/validator_derive_tests/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/validator_derive_tests/tests/compile-fail/custom/custom_not_string.rs b/validator_derive_tests/tests/compile-fail/custom/custom_not_string.rs index fecd0718..8fe6e533 100644 --- a/validator_derive_tests/tests/compile-fail/custom/custom_not_string.rs +++ b/validator_derive_tests/tests/compile-fail/custom/custom_not_string.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(custom = 2)] + #[validate(custom(function = 2))] s: String, } diff --git a/validator_derive_tests/tests/compile-fail/custom/custom_not_string.stderr b/validator_derive_tests/tests/compile-fail/custom/custom_not_string.stderr index 6f866ae9..74f9148c 100644 --- a/validator_derive_tests/tests/compile-fail/custom/custom_not_string.stderr +++ b/validator_derive_tests/tests/compile-fail/custom/custom_not_string.stderr @@ -1,5 +1,9 @@ -error: Invalid attribute #[validate] on field `s`: invalid argument for `custom` validator: only strings are allowed - --> $DIR/custom_not_string.rs:5:25 +error: Invalid attribute #[validate(custom(...))] on field `s`: + + = note: Invalid argument for `custom` validator, only paths are allowed + = help: Try formating the argument like `path::to::function` or `"path::to::function"` + + --> tests/compile-fail/custom/custom_not_string.rs:5:34 | -5 | #[validate(custom = 2)] - | ^ +5 | #[validate(custom(function = 2))] + | ^ diff --git a/validator_derive_tests/tests/compile-fail/range/wrong_type.rs b/validator_derive_tests/tests/compile-fail/custom/defined_args_in_custom.rs similarity index 56% rename from validator_derive_tests/tests/compile-fail/range/wrong_type.rs rename to validator_derive_tests/tests/compile-fail/custom/defined_args_in_custom.rs index 341ddb2d..3e40b753 100644 --- a/validator_derive_tests/tests/compile-fail/range/wrong_type.rs +++ b/validator_derive_tests/tests/compile-fail/custom/defined_args_in_custom.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(range(min = 10.0, max = 12.0))] + #[validate(custom(function = "hello_world", arg = "(i64, i64)"))] s: String, } diff --git a/validator_derive_tests/tests/compile-fail/custom/defined_args_in_custom.stderr b/validator_derive_tests/tests/compile-fail/custom/defined_args_in_custom.stderr new file mode 100644 index 00000000..5bd35ebf --- /dev/null +++ b/validator_derive_tests/tests/compile-fail/custom/defined_args_in_custom.stderr @@ -0,0 +1,5 @@ +error: Unknown field: `arg` + --> tests/compile-fail/custom/defined_args_in_custom.rs:5:49 + | +5 | #[validate(custom(function = "hello_world", arg = "(i64, i64)"))] + | ^^^ diff --git a/validator_derive_tests/tests/compile-fail/custom/different_lifetime.rs b/validator_derive_tests/tests/compile-fail/custom/different_lifetime.rs index 145f74fa..cd20f4de 100644 --- a/validator_derive_tests/tests/compile-fail/custom/different_lifetime.rs +++ b/validator_derive_tests/tests/compile-fail/custom/different_lifetime.rs @@ -1,13 +1,18 @@ use validator::{Validate, ValidationError}; -fn hello_world(_: &str, _arg: &mut String) -> Result<(), ValidationError> { +fn hello_world(_: &str, _arg: &Arg) -> Result<(), ValidationError> { Ok(()) } #[derive(Validate)] +#[validate(context = "Arg<'a>")] struct Test { - #[validate(custom(function = "hello_world", arg = "&'a mut String"))] + #[validate(custom(function = "hello_world", use_context))] s: String, } +struct Arg<'a> { + arg: &'a str, +} + fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/custom/different_lifetime.stderr b/validator_derive_tests/tests/compile-fail/custom/different_lifetime.stderr index ef630f1d..a001e156 100644 --- a/validator_derive_tests/tests/compile-fail/custom/different_lifetime.stderr +++ b/validator_derive_tests/tests/compile-fail/custom/different_lifetime.stderr @@ -1,5 +1,9 @@ -error: Invalid argument reference: The lifetime `'a` is not supported. Please use the validator lifetime `'v_a` - --> $DIR/different_lifetime.rs:9:55 +error: Invalid argument reference + + = note: The lifetime `'a` is not supported. + = help: Please use the validator lifetime `'v_a` + + --> tests/compile-fail/custom/different_lifetime.rs:8:22 | -9 | #[validate(custom(function = "hello_world", arg = "&'a mut String"))] - | ^^^^^^^^^^^^^^^^ +8 | #[validate(context = "Arg<'a>")] + | ^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/custom/function_without_reference.rs b/validator_derive_tests/tests/compile-fail/custom/function_without_reference.rs deleted file mode 100644 index a211749a..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/function_without_reference.rs +++ /dev/null @@ -1,9 +0,0 @@ -use validator::Validate; - -#[derive(Validate)] -struct Test { - #[validate(custom(function = "hello_world", arg = "dyn FnOnce(&str) -> usize"))] - s: String, -} - -fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/custom/function_without_reference.stderr b/validator_derive_tests/tests/compile-fail/custom/function_without_reference.stderr deleted file mode 100644 index 3234016f..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/function_without_reference.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Invalid argument type: Custom arguments only allow tuples, number types and references using the lifetime `'v_a` - --> $DIR/function_without_reference.rs:5:55 - | -5 | #[validate(custom(function = "hello_world", arg = "dyn FnOnce(&str) -> usize"))] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.rs b/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.rs index 4468ba1a..43c13cfe 100644 --- a/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.rs +++ b/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(custom(arg = "i64"))] + #[validate(custom(use_context))] s: String, } diff --git a/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.stderr b/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.stderr index 7d7f4e9b..03e8c23c 100644 --- a/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.stderr +++ b/validator_derive_tests/tests/compile-fail/custom/missing_function_arg.stderr @@ -1,5 +1,5 @@ -error: Invalid attribute #[validate] on field `s`: The validator `custom` requires the `function` parameter. - --> $DIR/missing_function_arg.rs:5:5 +error: Missing field `function` + --> tests/compile-fail/custom/missing_function_arg.rs:5:16 | -5 | #[validate(custom(arg = "i64"))] - | ^ +5 | #[validate(custom(use_context))] + | ^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.rs b/validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.rs deleted file mode 100644 index 1896483e..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.rs +++ /dev/null @@ -1,9 +0,0 @@ -use validator::Validate; - -#[derive(Validate)] -struct Test { - #[validate(custom(function = "hello_world", arg = "i64, i64"))] - s: String, -} - -fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.stderr b/validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.stderr deleted file mode 100644 index ca1a6ed8..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/multiple_types_in_arg.stderr +++ /dev/null @@ -1,6 +0,0 @@ -error: Invalid attribute #[validate] on field `s`: invalid argument type for `arg` of `custom` validator: The string has to be a single type. - (Tip: You can combine multiple types into one tuple.) - --> $DIR/multiple_types_in_arg.rs:5:55 - | -5 | #[validate(custom(function = "hello_world", arg = "i64, i64"))] - | ^^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/custom/type_without_reference.rs b/validator_derive_tests/tests/compile-fail/custom/type_without_reference.rs deleted file mode 100644 index 3d95626a..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/type_without_reference.rs +++ /dev/null @@ -1,9 +0,0 @@ -use validator::Validate; - -#[derive(Validate)] -struct Test { - #[validate(custom(function = "hello_world", arg = "String"))] - s: String, -} - -fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/custom/type_without_reference.stderr b/validator_derive_tests/tests/compile-fail/custom/type_without_reference.stderr deleted file mode 100644 index 8b7aefdf..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/type_without_reference.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Invalid argument type: All types except numbers and tuples need be passed by reference using the lifetime `'v_a` - --> $DIR/type_without_reference.rs:5:55 - | -5 | #[validate(custom(function = "hello_world", arg = "String"))] - | ^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.rs b/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.rs deleted file mode 100644 index d75e8a5f..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.rs +++ /dev/null @@ -1,16 +0,0 @@ -use validator::{Validate, ValidationError}; - -fn hello_world(_: &str, _arg: i32) -> Result<(), ValidationError> { - Ok(()) -} - -#[derive(Validate)] -struct Test { - #[validate(custom(function = "hello_world", arg = "i32"))] - s: String, -} - -fn main() { - let test = Test{s: "Test".to_string()}; - test.validate(); -} diff --git a/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr b/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr deleted file mode 100644 index 04db8d36..00000000 --- a/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error[E0599]: no method named `validate` found for struct `Test` in the current scope - --> tests/compile-fail/custom/validate_not_impl_with_args.rs:15:10 - | -8 | struct Test { - | ----------- method `validate` not found for this struct -... -15 | test.validate(); - | ^^^^^^^^ method not found in `Test` - | - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `validate`, perhaps you need to implement it: - candidate #1: `Validate` diff --git a/validator_derive_tests/tests/compile-fail/length/equal_and_min_max_set.stderr b/validator_derive_tests/tests/compile-fail/length/equal_and_min_max_set.stderr index c64bc18f..9716ed74 100644 --- a/validator_derive_tests/tests/compile-fail/length/equal_and_min_max_set.stderr +++ b/validator_derive_tests/tests/compile-fail/length/equal_and_min_max_set.stderr @@ -1,5 +1,9 @@ -error: Invalid attribute #[validate] on field `s`: both `equal` and `min` or `max` have been set in `length` validator: probably a mistake - --> $DIR/equal_and_min_max_set.rs:5:32 +error: Invalid attribute #[validate(length(...))] on field `s`: + + = note: Both `equal` and `min` or `max` have been set + = help: Exclusively use either the `equal` or `min` and `max` attributes + + --> tests/compile-fail/length/equal_and_min_max_set.rs:5:40 | 5 | #[validate(length(min = 1, equal = 2))] - | ^^^^^ + | ^ diff --git a/validator_derive_tests/tests/compile-fail/length/no_args.rs b/validator_derive_tests/tests/compile-fail/length/no_args.rs index c3878e24..0537b892 100644 --- a/validator_derive_tests/tests/compile-fail/length/no_args.rs +++ b/validator_derive_tests/tests/compile-fail/length/no_args.rs @@ -2,6 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { + #[validate(range(min = 5, max = 10))] #[validate(length())] s: String, } diff --git a/validator_derive_tests/tests/compile-fail/length/no_args.stderr b/validator_derive_tests/tests/compile-fail/length/no_args.stderr index 2c7599fe..d492f1fa 100644 --- a/validator_derive_tests/tests/compile-fail/length/no_args.stderr +++ b/validator_derive_tests/tests/compile-fail/length/no_args.stderr @@ -1,5 +1,9 @@ -error: Invalid attribute #[validate] on field `s`: Validator `length` requires at least 1 argument out of `min`, `max` and `equal` - --> $DIR/no_args.rs:5:5 +error: Invalid attribute #[validate(length(...))] on field `s`: + + = note: Validator `length` requires at least 1 argument + = help: Add the argument `equal`, `min` or `max` + + --> tests/compile-fail/length/no_args.rs:6:5 | -5 | #[validate(length())] - | ^ +6 | #[validate(length())] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/length/unknown_arg.stderr b/validator_derive_tests/tests/compile-fail/length/unknown_arg.stderr index 7b230518..2d6640d9 100644 --- a/validator_derive_tests/tests/compile-fail/length/unknown_arg.stderr +++ b/validator_derive_tests/tests/compile-fail/length/unknown_arg.stderr @@ -1,5 +1,5 @@ -error: Invalid attribute #[validate] on field `s`: unknown argument `eq` for validator `length` (it only has `min`, `max`, `equal`) - --> $DIR/unknown_arg.rs:5:23 +error: Unknown field: `eq`. Did you mean `equal`? + --> tests/compile-fail/length/unknown_arg.rs:5:23 | 5 | #[validate(length(eq = 2))] | ^^ diff --git a/validator_derive_tests/tests/compile-fail/length/wrong_type.rs b/validator_derive_tests/tests/compile-fail/length/wrong_type.rs index 914a411e..6cf895a3 100644 --- a/validator_derive_tests/tests/compile-fail/length/wrong_type.rs +++ b/validator_derive_tests/tests/compile-fail/length/wrong_type.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(length())] + #[validate(length(min = 5))] s: usize, } diff --git a/validator_derive_tests/tests/compile-fail/length/wrong_type.stderr b/validator_derive_tests/tests/compile-fail/length/wrong_type.stderr index a48f9a4f..b3be09f6 100644 --- a/validator_derive_tests/tests/compile-fail/length/wrong_type.stderr +++ b/validator_derive_tests/tests/compile-fail/length/wrong_type.stderr @@ -1,5 +1,7 @@ -error: Validator `length` can only be used on types `String`, `&str`, Cow<'_,str>, `Vec`, slice, or map/set types (BTree/Hash/Index) but found `usize` for field `s` - --> $DIR/wrong_type.rs:6:8 +error[E0599]: no method named `validate_length` found for type `usize` in the current scope + --> tests/compile-fail/length/wrong_type.rs:3:10 | -6 | s: usize, - | ^^^^^ +3 | #[derive(Validate)] + | ^^^^^^^^ method not found in `usize` + | + = note: this error originates in the derive macro `Validate` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.rs b/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.rs index 275749c4..42501895 100644 --- a/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.rs +++ b/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(must_match = "password2")] + #[validate(must_match(other = password2))] password: String, } diff --git a/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.stderr b/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.stderr index 41235be2..87f9f9f2 100644 --- a/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.stderr +++ b/validator_derive_tests/tests/compile-fail/must_match/field_doesnt_exist.stderr @@ -1,5 +1,9 @@ -error: Invalid argument for `must_match` validator of field `password`: the other field doesn't exist in struct - --> $DIR/field_doesnt_exist.rs:5:5 +error: Invalid attribute for #[validate(must_match(...))] on field `password`: + + = note: The `other` field doesn't exist in the struct `Test` + = help: Add the field `password2` to the struct + + --> tests/compile-fail/must_match/field_doesnt_exist.rs:5:35 | -5 | #[validate(must_match = "password2")] - | ^ +5 | #[validate(must_match(other = password2))] + | ^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.rs b/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.rs index 5cbc1474..54839718 100644 --- a/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.rs +++ b/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(must_match = "password2")] + #[validate(must_match(other = "password2"))] password: String, password2: i32, } diff --git a/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.stderr b/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.stderr index 51c5f4a8..8d2b2511 100644 --- a/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.stderr +++ b/validator_derive_tests/tests/compile-fail/must_match/field_type_doesnt_match.stderr @@ -1,5 +1,17 @@ -error: Invalid argument for `must_match` validator of field `password`: types of field can't match - --> $DIR/field_type_doesnt_match.rs:5:5 +error[E0308]: mismatched types + --> tests/compile-fail/must_match/field_type_doesnt_match.rs:3:10 | -5 | #[validate(must_match = "password2")] - | ^ +3 | #[derive(Validate)] + | ^^^^^^^^ + | | + | expected `&String`, found `&i32` + | arguments to this function are incorrect + | + = note: expected reference `&String` + found reference `&i32` +note: function defined here + --> $WORKSPACE/validator/src/validation/must_match.rs + | + | pub fn validate_must_match(a: T, b: T) -> bool { + | ^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the derive macro `Validate` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.rs b/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.rs index 5b9fe264..4ab51c5c 100644 --- a/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.rs +++ b/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Email { - #[validate(not_a = "validator")] + #[validate(not_a(other = "validator"))] email: String, } diff --git a/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.stderr b/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.stderr index 63cf5868..d499d238 100644 --- a/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.stderr +++ b/validator_derive_tests/tests/compile-fail/must_match/unexpected_name_value.stderr @@ -1,5 +1,5 @@ -error: unexpected name value validator: "not_a" - --> $DIR/unexpected_name_value.rs:5:16 +error: Unknown field: `not_a` + --> tests/compile-fail/must_match/unexpected_name_value.rs:5:16 | -5 | #[validate(not_a = "validator")] +5 | #[validate(not_a(other = "validator"))] | ^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/no_nested_validations.rs b/validator_derive_tests/tests/compile-fail/no_nested_validations.rs index 4120d8b3..3440dfce 100644 --- a/validator_derive_tests/tests/compile-fail/no_nested_validations.rs +++ b/validator_derive_tests/tests/compile-fail/no_nested_validations.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate] + #[validate(nested)] nested: Nested, } diff --git a/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr b/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr index d7be9a97..580f537f 100644 --- a/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr +++ b/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr @@ -1,13 +1,13 @@ -error[E0599]: no method named `validate` found for struct `Nested` in the current scope +error[E0599]: no method named `validate_nested` found for struct `Nested` in the current scope --> tests/compile-fail/no_nested_validations.rs:3:10 | 3 | #[derive(Validate)] | ^^^^^^^^ method not found in `Nested` ... 9 | struct Nested { - | ------------- method `validate` not found for this struct + | ------------- method `validate_nested` not found for this struct | = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `validate`, perhaps you need to implement it: - candidate #1: `Validate` + = note: the following trait defines an item `validate_nested`, perhaps you need to implement it: + candidate #1: `ValidateNested` = note: this error originates in the derive macro `Validate` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/validator_derive_tests/tests/compile-fail/no_validations.rs b/validator_derive_tests/tests/compile-fail/no_validations.rs deleted file mode 100644 index fcfdcb7a..00000000 --- a/validator_derive_tests/tests/compile-fail/no_validations.rs +++ /dev/null @@ -1,9 +0,0 @@ -use validator::Validate; - -#[derive(Validate)] -struct Test { - #[validate()] - s: String, -} - -fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/no_validations.stderr b/validator_derive_tests/tests/compile-fail/no_validations.stderr deleted file mode 100644 index 2131ee8a..00000000 --- a/validator_derive_tests/tests/compile-fail/no_validations.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Invalid attribute #[validate] on field `s`: it needs at least one validator - --> $DIR/no_validations.rs:5:5 - | -5 | #[validate()] - | ^ diff --git a/validator_derive_tests/tests/compile-fail/not_a_struct.stderr b/validator_derive_tests/tests/compile-fail/not_a_struct.stderr index 885ba279..f2cd1a79 100644 --- a/validator_derive_tests/tests/compile-fail/not_a_struct.stderr +++ b/validator_derive_tests/tests/compile-fail/not_a_struct.stderr @@ -1,5 +1,7 @@ -error: #[derive(Validate)] can only be used with structs - --> $DIR/not_a_struct.rs:4:1 +error: Unsupported shape `enum`. Expected struct with named fields. + --> tests/compile-fail/not_a_struct.rs:3:10 | -4 | pub enum NotAStruct { - | ^^^ +3 | #[derive(Validate)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `Validate` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/validator_derive_tests/tests/compile-fail/range/no_args.stderr b/validator_derive_tests/tests/compile-fail/range/no_args.stderr index 5e6ef1ac..822b9b90 100644 --- a/validator_derive_tests/tests/compile-fail/range/no_args.stderr +++ b/validator_derive_tests/tests/compile-fail/range/no_args.stderr @@ -1,5 +1,9 @@ -error: Invalid attribute #[validate] on field `s`: Validator `range` requires at least 1 argument out of `min`, `max`, `exclusive_min` and `exclusive_max` - --> $DIR/no_args.rs:5:5 +error: Invalid attribute #[validate(range(...))] on field `s`: + + = note: Validator `range` requires at least 1 argument + = help: Add the argument `min` or `max`, `exclusive_min` or `exclusive_max` + + --> tests/compile-fail/range/no_args.rs:5:5 | 5 | #[validate(range())] - | ^ + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/range/unknown_arg.stderr b/validator_derive_tests/tests/compile-fail/range/unknown_arg.stderr index 5ff26d0b..5e9dee36 100644 --- a/validator_derive_tests/tests/compile-fail/range/unknown_arg.stderr +++ b/validator_derive_tests/tests/compile-fail/range/unknown_arg.stderr @@ -1,5 +1,5 @@ -error: Invalid attribute #[validate] on field `s`: unknown argument `mi` for validator `range` (it only has `min`, `max`) - --> $DIR/unknown_arg.rs:5:22 +error: Unknown field: `mi`. Did you mean `min`? + --> tests/compile-fail/range/unknown_arg.rs:5:22 | 5 | #[validate(range(mi = 2, max = 3))] | ^^ diff --git a/validator_derive_tests/tests/compile-fail/range/wrong_type.stderr b/validator_derive_tests/tests/compile-fail/range/wrong_type.stderr deleted file mode 100644 index f1ab4477..00000000 --- a/validator_derive_tests/tests/compile-fail/range/wrong_type.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Validator `range` can only be used on number types but found `String` for field `s` - --> $DIR/wrong_type.rs:6:8 - | -6 | s: String, - | ^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/schema/missing_function.stderr b/validator_derive_tests/tests/compile-fail/schema/missing_function.stderr index 4c957ddb..953f1f71 100644 --- a/validator_derive_tests/tests/compile-fail/schema/missing_function.stderr +++ b/validator_derive_tests/tests/compile-fail/schema/missing_function.stderr @@ -1,5 +1,5 @@ -error: Invalid schema level validation: `function` is required - --> $DIR/missing_function.rs:4:12 +error: Missing field `function` + --> tests/compile-fail/schema/missing_function.rs:4:12 | 4 | #[validate(schema())] | ^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/unexpected_list_validator.stderr b/validator_derive_tests/tests/compile-fail/unexpected_list_validator.stderr index 75ed20a1..ac9ba3bb 100644 --- a/validator_derive_tests/tests/compile-fail/unexpected_list_validator.stderr +++ b/validator_derive_tests/tests/compile-fail/unexpected_list_validator.stderr @@ -1,5 +1,5 @@ -error: unexpected list validator: "not_a_list" - --> $DIR/unexpected_list_validator.rs:5:16 +error: Unknown field: `not_a_list` + --> tests/compile-fail/unexpected_list_validator.rs:5:16 | 5 | #[validate(not_a_list(a, b, c))] | ^^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/unexpected_validator.stderr b/validator_derive_tests/tests/compile-fail/unexpected_validator.stderr index 4c58d691..46dcdab1 100644 --- a/validator_derive_tests/tests/compile-fail/unexpected_validator.stderr +++ b/validator_derive_tests/tests/compile-fail/unexpected_validator.stderr @@ -1,5 +1,5 @@ -error: Unexpected validator: my_custom_validator - --> $DIR/unexpected_validator.rs:5:16 +error: Unknown field: `my_custom_validator` + --> tests/compile-fail/unexpected_validator.rs:5:16 | 5 | #[validate(my_custom_validator)] | ^^^^^^^^^^^^^^^^^^^ diff --git a/validator_derive_tests/tests/compile-fail/unnamed_fields.stderr b/validator_derive_tests/tests/compile-fail/unnamed_fields.stderr index 86106398..d7cb7654 100644 --- a/validator_derive_tests/tests/compile-fail/unnamed_fields.stderr +++ b/validator_derive_tests/tests/compile-fail/unnamed_fields.stderr @@ -1,8 +1,7 @@ -error: struct has unnamed fields - - = help: #[derive(Validate)] can only be used on structs with named fields - - --> $DIR/unnamed_fields.rs:4:19 +error: Unsupported shape `one unnamed field`. Expected named fields. + --> tests/compile-fail/unnamed_fields.rs:3:10 | -4 | struct TupleStruct(String); - | ^^^^^^^^ +3 | #[derive(Validate)] + | ^^^^^^^^ + | + = note: this error originates in the derive macro `Validate` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/validator_derive_tests/tests/compile_test.rs b/validator_derive_tests/tests/compile_test.rs index d2642518..bc67e4e0 100644 --- a/validator_derive_tests/tests/compile_test.rs +++ b/validator_derive_tests/tests/compile_test.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "test_ui")] #[test] fn ui() { let t = trybuild::TestCases::new(); diff --git a/validator_derive_tests/tests/complex.rs b/validator_derive_tests/tests/complex.rs index d3be6692..047c1204 100644 --- a/validator_derive_tests/tests/complex.rs +++ b/validator_derive_tests/tests/complex.rs @@ -22,14 +22,15 @@ fn validate_signup(data: &SignupData) -> Result<(), ValidationError> { Ok(()) } +#[allow(unused)] #[derive(Debug, Validate, Deserialize)] -#[validate(schema(function = "validate_signup", skip_on_field_errors = false))] +#[validate(schema(function = validate_signup))] struct SignupData { #[validate(email)] mail: String, #[validate(url)] site: String, - #[validate(length(min = 1), custom = "validate_unique_username")] + #[validate(length(min = 1), custom(function = validate_unique_username))] #[serde(rename = "firstName")] first_name: String, #[validate(range(min = 18, max = 20))] @@ -70,66 +71,6 @@ fn is_fine_with_many_valid_validations() { assert!(signup.validate().is_ok()); } -#[test] -fn failed_validation_points_to_original_field_name() { - let signup = SignupData { - mail: "bob@bob.com".to_string(), - site: "http://hello.com".to_string(), - first_name: "".to_string(), - age: 18, - card: Some(Card { number: "1234567890123456".to_string(), cvv: 1 }), - preferences: vec![Preference { name: "abc".to_string(), value: true }], - }; - let res = signup.validate(); - // println!("{}", serde_json::to_string(&res).unwrap()); - assert!(res.is_err()); - let err = res.unwrap_err(); - let errs = err.errors(); - assert!(errs.contains_key("firstName")); - if let ValidationErrorsKind::Field(ref err) = errs["firstName"] { - assert_eq!(err.len(), 1); - assert_eq!(err[0].code, "length"); - } else { - panic!("Expected field validation errors"); - } - assert!(errs.contains_key("card")); - if let ValidationErrorsKind::Struct(ref errs) = errs["card"] { - unwrap_map(errs, |errs| { - assert_eq!(errs.len(), 2); - assert!(errs.contains_key("number")); - if let ValidationErrorsKind::Field(ref err) = errs["number"] { - assert_eq!(err.len(), 1); - assert_eq!(err[0].code, "credit_card"); - } else { - panic!("Expected field validation errors"); - } - assert!(errs.contains_key("cvv")); - if let ValidationErrorsKind::Field(ref err) = errs["cvv"] { - assert_eq!(err.len(), 1); - assert_eq!(err[0].code, "range"); - } else { - panic!("Expected field validation errors"); - } - }); - } else { - panic!("Expected struct validation errors"); - } - assert!(errs.contains_key("preferences")); - if let ValidationErrorsKind::List(ref errs) = errs["preferences"] { - assert!(errs.contains_key(&0)); - unwrap_map(&errs[&0], |errs| { - assert_eq!(errs.len(), 1); - assert!(errs.contains_key("name")); - if let ValidationErrorsKind::Field(ref err) = errs["name"] { - assert_eq!(err.len(), 1); - assert_eq!(err[0].code, "length"); - } - }); - } else { - panic!("Expected list validation errors"); - } -} - #[test] fn test_can_validate_option_fields_with_lifetime() { lazy_static! { @@ -150,15 +91,15 @@ fn test_can_validate_option_fields_with_lifetime() { email: Option<&'a str>, #[validate(url)] url: Option<&'a str>, - #[validate(contains = "@")] + #[validate(contains(pattern = "@"))] text: Option<&'a str>, - #[validate(regex = "RE2")] + #[validate(regex(path = *RE2))] re: Option<&'a str>, - #[validate(custom = "check_str")] + #[validate(custom(function = check_str))] custom: Option<&'a str>, } - fn check_str(_: &str) -> Result<(), ValidationError> { + fn check_str(_: &Option<&str>) -> Result<(), ValidationError> { Ok(()) } @@ -200,15 +141,15 @@ fn test_can_validate_option_fields_without_lifetime() { email: Option, #[validate(url)] url: Option, - #[validate(contains = "@")] + #[validate(contains(pattern = "@"))] text: Option, - #[validate(regex = "RE2")] + #[validate(regex(path = *RE2))] re: Option, - #[validate(custom = "check_str")] + #[validate(custom(function = check_str))] custom: Option, } - fn check_str(_: &str) -> Result<(), ValidationError> { + fn check_str(_: &Option) -> Result<(), ValidationError> { Ok(()) } @@ -230,6 +171,10 @@ fn test_can_validate_option_fields_without_lifetime() { #[test] fn test_works_with_question_mark_operator() { + fn _check_str(_: &String) -> Result<(), ValidationError> { + Ok(()) + } + fn some_fn() -> Result<(), ValidationErrors> { let signup = SignupData { mail: "invalid_email".to_string(), @@ -269,6 +214,7 @@ fn test_works_with_none_values() { assert!(q.validate().is_ok()); } +#[allow(dead_code)] fn unwrap_map(errors: &ValidationErrors, f: F) where F: FnOnce(HashMap<&'static str, ValidationErrorsKind>), diff --git a/validator_derive_tests/tests/contains.rs b/validator_derive_tests/tests/contains.rs index 21f9e185..d8aae76a 100644 --- a/validator_derive_tests/tests/contains.rs +++ b/validator_derive_tests/tests/contains.rs @@ -4,7 +4,7 @@ use validator::Validate; fn can_validate_contains_ok() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(contains = "he")] + #[validate(contains(pattern = "he"))] val: String, } @@ -17,7 +17,7 @@ fn can_validate_contains_ok() { fn value_not_containing_needle_fails_validation() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(contains = "he")] + #[validate(contains(pattern = "he"))] val: String, } diff --git a/validator_derive_tests/tests/credit_card.rs b/validator_derive_tests/tests/credit_card.rs index 04235c8c..484f94af 100644 --- a/validator_derive_tests/tests/credit_card.rs +++ b/validator_derive_tests/tests/credit_card.rs @@ -84,8 +84,8 @@ fn can_validate_custom_impl_for_credit_card() { val: CustomCreditCard, } - impl validator::ValidateCreditCard for &CustomCreditCard { - fn to_credit_card_string(&self) -> Cow { + impl validator::ValidateCreditCard for CustomCreditCard { + fn as_credit_card_string(&self) -> Cow { Cow::from(format!("{}{}{}", &self.bin, &self.ian, &self.check,)) } } diff --git a/validator_derive_tests/tests/custom.rs b/validator_derive_tests/tests/custom.rs index 8fe402cf..3d9187a5 100644 --- a/validator_derive_tests/tests/custom.rs +++ b/validator_derive_tests/tests/custom.rs @@ -1,10 +1,10 @@ use validator::{Validate, ValidationError}; -fn valid_custom_fn(_: &str) -> Result<(), ValidationError> { +fn valid_custom_fn(_: &String) -> Result<(), ValidationError> { Ok(()) } -fn invalid_custom_fn(_: &str) -> Result<(), ValidationError> { +fn invalid_custom_fn(_: &String) -> Result<(), ValidationError> { Err(ValidationError::new("meh")) } @@ -12,7 +12,7 @@ fn invalid_custom_fn(_: &str) -> Result<(), ValidationError> { fn can_validate_custom_fn_ok() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(custom = "valid_custom_fn")] + #[validate(custom(function = valid_custom_fn))] val: String, } @@ -25,7 +25,7 @@ fn can_validate_custom_fn_ok() { fn can_fail_custom_fn_validation() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(custom = "invalid_custom_fn")] + #[validate(custom(function = invalid_custom_fn))] val: String, } @@ -44,7 +44,7 @@ fn can_fail_custom_fn_validation() { fn can_specify_message_for_custom_fn() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(custom(function = "invalid_custom_fn", message = "oops"))] + #[validate(custom(function = invalid_custom_fn, message = "oops"))] val: String, } let s = TestStruct { val: String::new() }; @@ -56,3 +56,50 @@ fn can_specify_message_for_custom_fn() { assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); } + +#[test] +fn can_specify_code_for_custom_fn() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(custom(function = invalid_custom_fn, code = "custom_validation"))] + val: String, + } + let s = TestStruct { val: String::new() }; + let res = s.validate(); + assert!(res.is_err()); + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].clone().code, "custom_validation"); +} + +#[test] +fn can_nest_custom_validations() { + #[derive(Validate)] + struct TestStruct { + #[validate(nested)] + a: A, + } + + #[derive(Validate)] + #[validate(nested)] + struct A { + #[validate(custom(function = custom_fn))] + val: String, + } + + fn custom_fn(val: &String) -> Result<(), ValidationError> { + if val == "value" { + Ok(()) + } else { + Err(ValidationError::new("Invalid string")) + } + } + + let t = TestStruct { a: A { val: "value".to_string() } }; + assert!(t.validate().is_ok()); + + let t = TestStruct { a: A { val: "invalid value".to_string() } }; + assert!(t.validate().is_err()); +} diff --git a/validator_derive_tests/tests/custom_args.rs b/validator_derive_tests/tests/custom_args.rs index 7922b402..9c2fb331 100644 --- a/validator_derive_tests/tests/custom_args.rs +++ b/validator_derive_tests/tests/custom_args.rs @@ -2,117 +2,197 @@ use std::ops::AddAssign; use validator::{Validate, ValidateArgs, ValidationError}; -struct CustomStruct { - pub counter: i32, +#[derive(Debug, PartialEq, Clone)] +struct TestContext { + val: String, } -fn valid_generic_custom_i32(_: &T, _arg: i32) -> Result<(), ValidationError> { +fn valid_fn(_: &String, _arg: &TestContext) -> Result<(), ValidationError> { Ok(()) } -fn invalid_custom_tuple(_: &str, _arg: (i64, i64)) -> Result<(), ValidationError> { - Err(ValidationError::new("meh")) +fn valid_fn_with_ref(_: &String, _arg: &TestContext) -> Result<(), ValidationError> { + Ok(()) } -fn valid_reference_with_lifetime(_: &str, arg: &mut CustomStruct) -> Result<(), ValidationError> { - arg.counter += 1; +fn valid_fn_with_mut_ref(_: &String, arg: &mut TestContext) -> Result<(), ValidationError> { + arg.val = "new value".to_string(); Ok(()) } -fn invalid_validation_complex_args<'a, T: AddAssign>( - _: &str, - arg: (&'a mut CustomStruct, &'a mut T, T), -) -> Result<(), ValidationError> { - arg.0.counter += 1; - *arg.1 += arg.2; - Err(ValidationError::new("meh")) +#[test] +fn validate_simple_custom_fn() { + #[derive(Validate)] + #[validate(context = TestContext)] + struct TestStruct { + #[validate(custom(function = valid_fn, use_context))] + value: String, + } + + let test_struct = TestStruct { value: "Something".to_string() }; + let c = TestContext { val: "asd".to_string() }; + assert!(test_struct.validate_with_args(&c).is_ok()); } #[test] -fn validate_custom_fn_reference_with_lifetime_ok() { - #[derive(Debug, Validate)] +fn validate_multiple_custom_fn() { + #[derive(Validate)] + #[validate(context = TestContext)] struct TestStruct { - #[validate(custom( - function = "valid_reference_with_lifetime", - arg = "&'v_a mut CustomStruct" - ))] + #[validate(custom(function = valid_fn, use_context))] value: String, + #[validate(custom(function = valid_fn, use_context))] + value2: String, + #[validate(custom(function = valid_fn, use_context))] + value3: String, } - let s = TestStruct { value: "Hello World".to_string() }; + let test_struct = TestStruct { + value: "Something".to_string(), + value2: "asd".to_string(), + value3: "fgre".to_string(), + }; + + let test_arg = TestContext { val: "test".to_string() }; - let mut cs = CustomStruct { counter: 0 }; - assert!(s.validate_args(&mut cs).is_ok()); - assert!(cs.counter == 1); + assert!(test_struct.validate_with_args(&test_arg).is_ok()); } #[test] -fn validate_custom_fn_tuple_err() { - #[derive(Debug, Validate)] +fn validate_custom_fn_with_ref() { + #[derive(Validate)] + #[validate(context = TestContext)] struct TestStruct { - #[validate(custom(function = "invalid_custom_tuple", arg = "(i64, i64)"))] + #[validate(custom(function = valid_fn_with_ref, use_context))] value: String, } - let s = TestStruct { value: "Hello World".to_string() }; + let val = TestContext { val: "asd".to_string() }; + let test_struct = TestStruct { value: "Something".to_string() }; + assert!(test_struct.validate_with_args(&val).is_ok()); - assert!(s.validate_args((77, 555)).is_err()); + // test reference + assert_eq!(val, TestContext { val: "asd".to_string() }); } #[test] -fn validate_custom_struct_generic_and_lifetime_fn_i32_ok() { - #[derive(Debug, Validate)] - struct TestGenericStruct<'a, T: serde::ser::Serialize> { - #[validate(custom(function = "valid_generic_custom_i32", arg = "i32"))] - generic: &'a T, +fn validate_custom_fn_with_mut_ref() { + #[derive(Validate)] + #[validate(context = TestContext, mutable)] + struct TestStruct<'a> { + #[validate(custom(function = valid_fn_with_mut_ref, use_context))] + value: &'a mut String, } - let int_128 = 746460_i128; - let s = TestGenericStruct { generic: &int_128 }; + let mut val = TestContext { val: "old value".to_string() }; + let test_struct = TestStruct { value: &mut "Something".to_string() }; + assert!(test_struct.validate_with_args(&mut val).is_ok()); - assert!(s.validate_args(16).is_ok()); + assert_eq!(val, TestContext { val: "new value".to_string() }); } #[test] -fn invalidate_custom_fn_complex_arg_err() { - #[derive(Debug, Validate)] +fn validate_custom_fn_with_complex_args() { + #[derive(Validate)] + #[validate(context = "Arg", mutable)] struct TestStruct { - #[validate(custom( - function = "invalid_validation_complex_args", - arg = "(&'v_a mut CustomStruct, &'v_a mut i32, i32)" - ))] + #[validate(custom(function = add_assign, use_context))] value: String, } - let s = TestStruct { value: "Hello World".to_string() }; + struct Arg { + counter: T, + } + + fn add_assign(_value: &str, arg: &mut Arg) -> Result<(), ValidationError> { + arg.counter += 1; + Ok(()) + } + + let mut arg = Arg { counter: 0 }; + let test_struct = TestStruct { value: "test".to_string() }; + let res = test_struct.validate_with_args(&mut arg); + assert!(res.is_ok()); - let mut cs = CustomStruct { counter: 0 }; - let mut value = 10; - assert!(s.validate_args((&mut cs, &mut value, 5)).is_err()); - assert!(cs.counter == 1); - assert!(value == 15); + assert_eq!(arg.counter, 1); } #[test] -fn validate_custom_multiple_fn_with_args_ok() { - #[derive(Debug, Validate)] +fn validate_custom_fn_with_multiple_args() { + #[derive(Debug, Validate, PartialEq)] + #[validate(context = Arg, mutable)] struct TestStruct { - #[validate(custom( - function = "valid_reference_with_lifetime", - arg = "&'v_a mut CustomStruct" - ))] + #[validate(custom(function = add_assign, use_context))] value: String, + } - #[validate(custom(function = "invalid_custom_tuple", arg = "(i64, i64)"))] - other_value: String, + #[derive(Debug, PartialEq)] + struct Arg { + counter: i32, + counter2: u8, } - let s = TestStruct { - value: "Hello World".to_string(), - other_value: "I'm different from value".to_string(), - }; + fn add_assign(_: &String, arg: &mut Arg) -> Result<(), ValidationError> { + arg.counter += 1; + arg.counter2 += 2; + Ok(()) + } + + let test_struct = TestStruct { value: "something".to_string() }; + let mut arg = Arg { counter: 5, counter2: 16 }; + assert!(test_struct.validate_with_args(&mut arg).is_ok()); + + assert_eq!(arg, Arg { counter: 6, counter2: 18 }); +} + +#[test] +fn validate_nested_custom_fn() { + #[derive(Validate)] + #[validate(context = Arg)] + struct TestStruct { + #[validate(nested)] + child: Child, + } + + #[derive(Validate)] + #[validate(context = Arg, nested)] + struct Child { + #[validate(custom(function = add_assign, use_context))] + value: String, + } + + struct Arg { + _counter: i32, + } + + fn add_assign(_: &String, _arg: &Arg) -> Result<(), ValidationError> { + Ok(()) + } + + let t = TestStruct { child: Child { value: "test".to_string() } }; + let arg = Arg { _counter: 123 }; + + assert!(t.validate_with_args(&arg).is_ok()); +} + +#[test] +fn validate_custom_arg_with_lifetime() { + #[derive(Validate)] + #[validate(context = "Arg<'v_a>")] + struct Test { + #[validate(custom(function = custom_with_lifetime, use_context))] + val: String, + } + + struct Arg<'a> { + _arg: &'a str, + } + + fn custom_with_lifetime(_: &str, _: &Arg) -> Result<(), ValidationError> { + Ok(()) + } - let mut cs = CustomStruct { counter: 0 }; - assert!(s.validate_args((&mut cs, (123, 456))).is_err()); - assert!(cs.counter == 1); + let test = Test { val: "Test".to_string() }; + let arg = Arg { _arg: "Test" }; + assert!(test.validate_with_args(&arg).is_ok()); } diff --git a/validator_derive_tests/tests/does_not_contain.rs b/validator_derive_tests/tests/does_not_contain.rs index bb175e26..63257879 100644 --- a/validator_derive_tests/tests/does_not_contain.rs +++ b/validator_derive_tests/tests/does_not_contain.rs @@ -5,7 +5,7 @@ use validator::Validate; fn can_validate_does_not_contain_ok() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(does_not_contain = "asdf")] + #[validate(does_not_contain(pattern = "asdf"))] val: String, } @@ -18,7 +18,7 @@ fn can_validate_does_not_contain_ok() { fn container_containing_needle_fails_validation() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(does_not_contain = "asdf")] + #[validate(does_not_contain(pattern = "asdf"))] val: HashMap, } @@ -42,7 +42,7 @@ fn container_containing_needle_fails_validation() { fn string_containing_needle_fails_validation() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(does_not_contain = "he")] + #[validate(does_not_contain(pattern = "he"))] val: String, } diff --git a/validator_derive_tests/tests/email.rs b/validator_derive_tests/tests/email.rs index 0eaf4ca6..7af76851 100644 --- a/validator_derive_tests/tests/email.rs +++ b/validator_derive_tests/tests/email.rs @@ -83,9 +83,9 @@ fn can_validate_custom_impl_for_email() { val: CustomEmail, } - impl validator::ValidateEmail for &CustomEmail { - fn to_email_string(&self) -> Cow<'_, str> { - Cow::from(format!("{}@{}", self.user_part, self.domain_part)) + impl validator::ValidateEmail for CustomEmail { + fn as_email_string(&self) -> Option> { + Some(Cow::from(format!("{}@{}", self.user_part, self.domain_part))) } } diff --git a/validator_derive_tests/tests/ip.rs b/validator_derive_tests/tests/ip.rs new file mode 100644 index 00000000..a2504e23 --- /dev/null +++ b/validator_derive_tests/tests/ip.rs @@ -0,0 +1,181 @@ +use serde::Serialize; +use validator::Validate; + +#[test] +fn can_validate_ipv4() { + #[derive(Validate)] + struct TestStruct { + #[validate(ip(v4))] + val: String, + } + + let s = TestStruct { val: "192.168.1.1".to_string() }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn can_validate_ipv6() { + #[derive(Validate)] + struct TestStruct { + #[validate(ip(v6))] + val: String, + } + + let s = TestStruct { val: "2001:0db8:85a3:0000:0000:8a2e:0370:7334".to_string() }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn can_validate_ip() { + #[derive(Validate)] + struct TestStruct { + #[validate(ip)] + val: String, + } + + let s = TestStruct { val: "2001:0db8:85a3:0000:0000:8a2e:0370:7334".to_string() }; + assert!(s.validate().is_ok()); + + let s = TestStruct { val: "192.168.1.1".to_string() }; + assert!(s.validate().is_ok()); +} + +#[test] +fn bad_ip_fails_validation() { + #[derive(Validate)] + struct TestStruct { + #[validate(ip)] + val: String, + } + + let s = TestStruct { val: "123.123.123".to_string() }; + let res = s.validate(); + assert!(res.is_err()); + + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "ip"); + assert_eq!(errs["val"][0].params["value"], "123.123.123"); +} + +#[test] +fn bad_ipv4_fails_validation() { + #[derive(Validate)] + struct TestStruct { + #[validate(ip(v4))] + val: String, + } + + let s = TestStruct { val: "123.123.123".to_string() }; + let res = s.validate(); + assert!(res.is_err()); + + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "ip"); + assert_eq!(errs["val"][0].params["value"], "123.123.123"); +} + +#[test] +fn can_specify_code_for_ip() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(ip(code = "oops"))] + val: String, + } + + let s = TestStruct { val: "123.123.123".to_string() }; + let res = s.validate(); + assert!(res.is_err()); + + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_code_for_ipv4() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(ip(v4, code = "oops"))] + val: String, + } + + let s = TestStruct { val: "123.123.123".to_string() }; + let res = s.validate(); + assert!(res.is_err()); + + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_message_for_ip() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(ip(message = "oops"))] + val: String, + } + let s = TestStruct { val: "123.123.123".to_string() }; + let res = s.validate(); + assert!(res.is_err()); + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); +} + +#[test] +fn can_specify_message_for_ipv6() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(ip(v6, message = "oops"))] + val: String, + } + let s = TestStruct { val: "123.123.123".to_string() }; + let res = s.validate(); + assert!(res.is_err()); + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); +} + +#[test] +fn can_validate_custom_impl_for_ip() { + #[derive(Serialize)] + struct CustomIp { + a: u8, + b: u8, + c: u8, + d: u8, + } + + #[derive(Validate)] + struct TestStruct { + #[validate(ip)] + val: CustomIp, + } + + impl ToString for CustomIp { + fn to_string(&self) -> String { + format!("{}.{}.{}.{}", self.a, self.b, self.c, self.d) + } + } + + let valid = TestStruct { val: CustomIp { a: 192, b: 168, c: 1, d: 1 } }; + assert!(valid.validate().is_ok()); +} diff --git a/validator_derive_tests/tests/length.rs b/validator_derive_tests/tests/length.rs index f8529d5c..2bf3ece6 100644 --- a/validator_derive_tests/tests/length.rs +++ b/validator_derive_tests/tests/length.rs @@ -3,9 +3,6 @@ use validator::Validate; const MIN_CONST: u64 = 1; const MAX_CONST: u64 = 10; -const MAX_CONST_I32: i32 = 2; -const NEGATIVE_CONST_I32: i32 = -10; - #[test] fn can_validate_length_ok() { #[derive(Debug, Validate)] @@ -45,32 +42,6 @@ fn validate_length_with_ref_fails() { assert!(s.validate().is_err()); } -#[test] -fn validate_length_with_ref_i32_fails() { - #[derive(Debug, Validate)] - struct TestStruct { - #[validate(length(max = "MAX_CONST_I32"))] - val: String, - } - - let s = TestStruct { val: "TO_LONG_YAY".to_string() }; - - assert!(s.validate().is_err()); -} - -#[test] -fn validate_length_with_ref_negative_i32_fails() { - #[derive(Debug, Validate)] - struct TestStruct { - #[validate(length(max = "NEGATIVE_CONST_I32"))] - val: String, - } - - let s = TestStruct { val: "TO_LONG_YAY".to_string() }; - - assert!(s.validate().is_ok()); -} - #[test] fn value_out_of_length_fails_validation() { #[derive(Debug, Validate)] @@ -230,30 +201,9 @@ fn can_validate_custom_impl_for_length() { #[derive(Debug, Serialize)] struct CustomString(String); - impl validator::ValidateLength for &CustomString { - fn validate_length(&self, min: Option, max: Option, equal: Option) -> bool { - let length = self.length(); - - if let Some(eq) = equal { - return length == eq; - } else { - if let Some(m) = min { - if length < m { - return false; - } - } - if let Some(m) = max { - if length > m { - return false; - } - } - } - - true - } - - fn length(&self) -> u64 { - self.0.chars().count() as u64 + impl validator::ValidateLength for CustomString { + fn length(&self) -> Option { + Some(self.0.chars().count() as u64) } } diff --git a/validator_derive_tests/tests/must_match.rs b/validator_derive_tests/tests/must_match.rs index add825bb..922cc32d 100644 --- a/validator_derive_tests/tests/must_match.rs +++ b/validator_derive_tests/tests/must_match.rs @@ -4,7 +4,7 @@ use validator::Validate; fn can_validate_valid_must_match() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(must_match = "val2")] + #[validate(must_match(other = "val2"))] val: String, val2: String, } @@ -18,7 +18,7 @@ fn can_validate_valid_must_match() { fn not_matching_fails_validation() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(must_match = "val2")] + #[validate(must_match(other = "val2"))] val: String, val2: String, } diff --git a/validator_derive_tests/tests/nest_all_fields.rs b/validator_derive_tests/tests/nest_all_fields.rs new file mode 100644 index 00000000..4d8dc628 --- /dev/null +++ b/validator_derive_tests/tests/nest_all_fields.rs @@ -0,0 +1,58 @@ +use validator::Validate; + +#[test] +fn field_without_attribute_ignored() { + #[derive(Validate)] + struct Test { + _a: Nested, + #[validate(nested)] + b: NestedValidated, + } + + struct Nested { + _val: String, + } + + #[derive(Validate)] + #[validate(nested)] + struct NestedValidated { + #[validate(length(min = 5, max = 10))] + val: String, + } + + let test = Test { + _a: Nested { _val: "test".to_string() }, + b: NestedValidated { val: "valid str".to_string() }, + }; + + assert!(test.validate().is_ok()); +} + +#[test] +fn nest_all_fields_attribute_works() { + #[derive(Validate)] + #[validate(nest_all_fields)] + struct Test { + #[validate(skip)] + _a: Nested, + b: NestedValidated, + } + + struct Nested { + _val: String, + } + + #[derive(Validate)] + #[validate(nested)] + struct NestedValidated { + #[validate(length(min = 5, max = 10))] + val: String, + } + + let test = Test { + _a: Nested { _val: "test".to_string() }, + b: NestedValidated { val: "valid str".to_string() }, + }; + + assert!(test.validate().is_ok()); +} diff --git a/validator_derive_tests/tests/nested.rs b/validator_derive_tests/tests/nested.rs index 4b5fcf07..a23cf7d0 100644 --- a/validator_derive_tests/tests/nested.rs +++ b/validator_derive_tests/tests/nested.rs @@ -4,181 +4,154 @@ use std::{ collections::{HashMap, HashSet}, }; use validator::{ - validate_length, Validate, ValidationError, ValidationErrors, ValidationErrorsKind, + Validate, ValidateLength, ValidationError, ValidationErrors, ValidationErrorsKind, }; -#[derive(Debug, Validate)] -struct Root<'a> { - #[validate(length(min = 1))] - value: String, - - #[validate] - a: &'a A, -} - -#[derive(Debug, Validate)] -struct A { - #[validate(length(min = 1))] - value: String, - - #[validate] - b: B, -} - -#[derive(Debug, Validate)] -struct B { - #[validate(length(min = 1))] - value: String, -} - -#[derive(Debug, Validate)] -struct ParentWithOptionalChild { - #[validate] - child: Option, -} - -#[derive(Debug, Validate)] -struct ParentWithVectorOfChildren { - #[validate] - #[validate(length(min = 1))] - child: Vec, -} - -#[derive(Debug, Validate)] -struct ParentWithSliceOfChildren<'a> { - #[validate] - #[validate(length(min = 1))] - child: &'a [Child], -} - -#[derive(Debug, Validate)] -struct ParentWithArrayOfChildren { - #[validate] - #[validate(length(min = 1))] - child: [Child; 4], -} - -#[derive(Debug, Validate)] -struct ParentWithOptionVectorOfChildren { - #[validate] - #[validate(length(min = 1))] - child: Option>, -} +#[test] +fn is_fine_with_nested_validations() { + #[derive(Validate)] + struct Root<'a> { + #[validate(length(min = 5, max = 10))] + value: String, + #[validate(nested)] + a: &'a A, + } -#[derive(Debug, Validate)] -struct ParentWithMapOfChildren { - #[validate] - #[validate(length(min = 1))] - child: HashMap, -} + #[derive(Validate)] + #[validate(nested)] + struct A { + #[validate(length(min = 5, max = 10))] + value: String, + #[validate(nested)] + b: B, + } -#[derive(Debug, Validate)] -struct ParentWithRefMapOfChildren<'a> { - #[validate] - #[validate(length(min = 1))] - child: &'a HashMap, -} + #[derive(Validate)] + #[validate(nested)] + struct B { + #[validate(length(min = 5, max = 10))] + value: String, + } -#[derive(Debug, Validate)] -struct ParentWithOptionMapOfChildren { - #[validate] - #[validate(length(min = 1))] - child: Option>, -} + let root = Root { + value: "valid".to_string(), + a: &A { value: "valid".to_string(), b: B { value: "valid".to_string() } }, + }; -#[derive(Debug, Validate)] -struct ParentWithSetOfChildren { - #[validate] - #[validate(length(min = 1))] - child: HashSet, + assert!(root.validate().is_ok()); } -#[derive(Debug, Validate)] -struct ParentWithRefSetOfChildren<'a> { - #[validate] - #[validate(length(min = 1))] - child: &'a HashSet, -} +#[test] +fn fails_nested_validation() { + #[derive(Validate)] + struct Root<'a> { + #[validate(length(min = 5, max = 10))] + value: String, + #[validate(nested)] + a: &'a A, + } -#[derive(Debug, Validate)] -struct ParentWithOptionSetOfChildren { - #[validate] - #[validate(length(min = 1))] - child: Option>, -} + #[derive(Validate)] + #[validate(nested)] + struct A { + #[validate(length(min = 5, max = 10))] + value: String, + #[validate(nested)] + b: B, + } -#[derive(Debug, Validate, Serialize, Clone, Hash, PartialEq, Eq)] -struct Child { - #[validate(length(min = 1))] - value: String, -} + #[derive(Validate)] + #[validate(nested)] + struct B { + #[validate(length(min = 5, max = 10))] + value: String, + } -#[test] -fn is_fine_with_nested_validations() { let root = Root { value: "valid".to_string(), - a: &A { value: "valid".to_string(), b: B { value: "valid".to_string() } }, + a: &A { value: "invalid value".to_string(), b: B { value: "valid".to_string() } }, }; - assert!(root.validate().is_ok()); -} + assert!(root.validate().is_err()); -#[test] -fn failed_validation_points_to_original_field_names() { let root = Root { - value: String::new(), - a: &A { value: String::new(), b: B { value: String::new() } }, + value: "valid".to_string(), + a: &A { value: "valid".to_string(), b: B { value: "invalid value".to_string() } }, }; - let res = root.validate(); - assert!(res.is_err()); - let err = res.unwrap_err(); - let errs = err.errors(); - assert_eq!(errs.len(), 2); - assert!(errs.contains_key("value")); - if let ValidationErrorsKind::Field(ref errs) = errs["value"] { - assert_eq!(errs.len(), 1); - assert_eq!(errs[0].code, "length"); - } else { - panic!("Expected field validation errors"); - } - assert!(errs.contains_key("a")); - if let ValidationErrorsKind::Struct(ref errs) = errs["a"] { - unwrap_map(errs, |errs| { - assert_eq!(errs.len(), 2); - assert!(errs.contains_key("value")); - if let ValidationErrorsKind::Field(ref errs) = errs["value"] { - assert_eq!(errs.len(), 1); - assert_eq!(errs[0].code, "length"); - } else { - panic!("Expected field validation errors"); - } - assert!(errs.contains_key("b")); - if let ValidationErrorsKind::Struct(ref errs) = errs["b"] { - unwrap_map(errs, |errs| { - assert_eq!(errs.len(), 1); - assert!(errs.contains_key("value")); - if let ValidationErrorsKind::Field(ref errs) = errs["value"] { - assert_eq!(errs.len(), 1); - assert_eq!(errs[0].code, "length"); - } else { - panic!("Expected field validation errors"); - } - }); - } else { - panic!("Expected struct validation errors"); - } - }); - } else { - panic!("Expected struct validation errors"); - } -} + assert!(root.validate().is_err()); +} + +// // #[test] +// // fn failed_validation_points_to_original_field_names() { +// // let root = Root { +// // value: String::new(), +// // _a: &A { value: String::new(), _b: B { value: String::new() } }, +// // }; + +// // let res = root.validate(); +// // assert!(res.is_err()); +// // let err = res.unwrap_err(); +// // let errs = err.errors(); +// // assert_eq!(errs.len(), 2); +// // assert!(errs.contains_key("value")); +// // if let ValidationErrorsKind::Field(ref errs) = errs["value"] { +// // assert_eq!(errs.len(), 1); +// // assert_eq!(errs[0].code, "length"); +// // } else { +// // panic!("Expected field validation errors"); +// // } +// // assert!(errs.contains_key("a")); +// // if let ValidationErrorsKind::Struct(ref errs) = errs["a"] { +// // unwrap_map(errs, |errs| { +// // assert_eq!(errs.len(), 2); +// // assert!(errs.contains_key("value")); +// // if let ValidationErrorsKind::Field(ref errs) = errs["value"] { +// // assert_eq!(errs.len(), 1); +// // assert_eq!(errs[0].code, "length"); +// // } else { +// // panic!("Expected field validation errors"); +// // } +// // assert!(errs.contains_key("b")); +// // if let ValidationErrorsKind::Struct(ref errs) = errs["b"] { +// // unwrap_map(errs, |errs| { +// // assert_eq!(errs.len(), 1); +// // assert!(errs.contains_key("value")); +// // if let ValidationErrorsKind::Field(ref errs) = errs["value"] { +// // assert_eq!(errs.len(), 1); +// // assert_eq!(errs[0].code, "length"); +// // } else { +// // panic!("Expected field validation errors"); +// // } +// // }); +// // } else { +// // panic!("Expected struct validation errors"); +// // } +// // }); +// // } else { +// // panic!("Expected struct validation errors"); +// // } +// // } #[test] fn test_can_validate_option_fields_without_lifetime() { + #[derive(Validate)] + struct ParentWithOptionalChild { + #[validate(nested)] + child: Option, + } + + #[derive(Validate)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithOptionalChild { child: Some(Child { value: String::new() }) }; let res = instance.validate(); + assert!(res.is_err()); let err = res.unwrap_err(); let errs = err.errors(); @@ -202,12 +175,19 @@ fn test_can_validate_option_fields_without_lifetime() { #[test] fn test_can_validate_option_fields_with_lifetime() { - #[derive(Debug, Validate)] + #[derive(Validate)] struct ParentWithLifetimeAndOptionalChild<'a> { - #[validate] + #[validate(nested)] child: Option<&'a Child>, } + #[derive(Validate)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let child = Child { value: String::new() }; let instance = ParentWithLifetimeAndOptionalChild { child: Some(&child) }; @@ -236,6 +216,19 @@ fn test_can_validate_option_fields_with_lifetime() { #[test] fn test_works_with_none_values() { + #[derive(Validate)] + struct ParentWithOptionalChild { + #[validate(nested)] + child: Option, + } + + #[derive(Validate)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithOptionalChild { child: None }; let res = instance.validate(); @@ -244,6 +237,20 @@ fn test_works_with_none_values() { #[test] fn test_can_validate_vector_fields() { + #[derive(Validate)] + struct ParentWithVectorOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: Vec, + } + + #[derive(Validate, Serialize)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithVectorOfChildren { child: vec![ Child { value: "valid".to_string() }, @@ -254,6 +261,7 @@ fn test_can_validate_vector_fields() { }; let res = instance.validate(); + assert!(res.is_err()); let err = res.unwrap_err(); let errs = err.errors(); @@ -289,6 +297,20 @@ fn test_can_validate_vector_fields() { #[test] fn test_can_validate_slice_fields() { + #[derive(Validate)] + struct ParentWithSliceOfChildren<'a> { + #[validate(length(min = 1))] + #[validate(nested)] + child: &'a [Child], + } + + #[derive(Validate, Serialize)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let child = vec![ Child { value: "valid".to_string() }, Child { value: String::new() }, @@ -298,6 +320,7 @@ fn test_can_validate_slice_fields() { let instance = ParentWithSliceOfChildren { child: &child }; let res = instance.validate(); + assert!(res.is_err()); let err = res.unwrap_err(); let errs = err.errors(); @@ -333,6 +356,20 @@ fn test_can_validate_slice_fields() { #[test] fn test_can_validate_array_fields() { + #[derive(Validate)] + struct ParentWithArrayOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: [Child; 4], + } + + #[derive(Validate, Serialize)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithArrayOfChildren { child: [ Child { value: "valid".to_string() }, @@ -378,6 +415,20 @@ fn test_can_validate_array_fields() { #[test] fn test_can_validate_option_vector_fields() { + #[derive(Validate)] + struct ParentWithOptionVectorOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: Option>, + } + + #[derive(Validate, Serialize)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithOptionVectorOfChildren { child: Some(vec![ Child { value: "valid".to_string() }, @@ -423,6 +474,20 @@ fn test_can_validate_option_vector_fields() { #[test] fn test_can_validate_map_fields() { + #[derive(Validate)] + struct ParentWithMapOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: HashMap, + } + + #[derive(Validate, Serialize, Clone)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithMapOfChildren { child: [(0, Child { value: String::new() })].iter().cloned().collect(), }; @@ -452,6 +517,20 @@ fn test_can_validate_map_fields() { #[test] fn test_can_validate_ref_map_fields() { + #[derive(Validate)] + struct ParentWithRefMapOfChildren<'a> { + #[validate(length(min = 1))] + #[validate(nested)] + child: &'a HashMap, + } + + #[derive(Validate, Serialize, Clone)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let child = [(0, Child { value: String::new() })].iter().cloned().collect(); let instance = ParentWithRefMapOfChildren { child: &child }; @@ -480,6 +559,20 @@ fn test_can_validate_ref_map_fields() { #[test] fn test_can_validate_option_map_fields() { + #[derive(Validate)] + struct ParentWithOptionMapOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: Option>, + } + + #[derive(Validate, Serialize, Clone)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithOptionMapOfChildren { child: Some([(0, Child { value: String::new() })].iter().cloned().collect()), }; @@ -509,6 +602,20 @@ fn test_can_validate_option_map_fields() { #[test] fn test_can_validate_set_fields() { + #[derive(Validate)] + struct ParentWithSetOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: HashSet, + } + + #[derive(Validate, Serialize, Clone, PartialEq, Eq, Hash)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithSetOfChildren { child: [Child { value: String::new() }].iter().cloned().collect(), }; @@ -538,6 +645,20 @@ fn test_can_validate_set_fields() { #[test] fn test_can_validate_ref_set_fields() { + #[derive(Validate)] + struct ParentWithRefSetOfChildren<'a> { + #[validate(length(min = 1))] + #[validate(nested)] + child: &'a HashSet, + } + + #[derive(Validate, Serialize, Clone, PartialEq, Eq, Hash)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let child = [Child { value: String::new() }].iter().cloned().collect(); let instance = ParentWithRefSetOfChildren { child: &child }; @@ -566,6 +687,20 @@ fn test_can_validate_ref_set_fields() { #[test] fn test_can_validate_option_set_fields() { + #[derive(Validate)] + struct ParentWithOptionSetOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: Option>, + } + + #[derive(Validate, Serialize, Clone, PartialEq, Eq, Hash)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithOptionSetOfChildren { child: Some([Child { value: String::new() }].iter().cloned().collect()), }; @@ -595,9 +730,24 @@ fn test_can_validate_option_set_fields() { #[test] fn test_field_validations_take_priority_over_nested_validations() { + #[derive(Validate)] + struct ParentWithVectorOfChildren { + #[validate(length(min = 1))] + #[validate(nested)] + child: Vec, + } + + #[derive(Validate, Serialize, Clone, PartialEq, Eq, Hash)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + let instance = ParentWithVectorOfChildren { child: Vec::new() }; let res = instance.validate(); + assert!(res.is_err()); let err = res.unwrap_err(); let errs = err.errors(); @@ -620,6 +770,13 @@ fn test_field_validation_errors_replaced_with_nested_validations_fails() { child: Vec, } + #[derive(Debug, Validate, Serialize)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + impl Validate for ParentWithOverridingStructValidations { // Evaluating structs after fields validations have discovered errors should fail because // field validations are expected to take priority over nested struct validations @@ -627,7 +784,7 @@ fn test_field_validation_errors_replaced_with_nested_validations_fails() { fn validate(&self) -> Result<(), ValidationErrors> { // First validate the length of the vector: let mut errors = ValidationErrors::new(); - if !validate_length(&self.child, Some(2u64), None, None) { + if !self.child.validate_length(Some(2u64), None, None) { let mut err = ValidationError::new("length"); err.add_param(Cow::from("min"), &2u64); err.add_param(Cow::from("value"), &&self.child); @@ -668,6 +825,13 @@ fn test_field_validations_evaluated_after_nested_validations_fails() { child: Vec, } + #[derive(Debug, Validate, Serialize)] + #[validate(nested)] + struct Child { + #[validate(length(min = 1))] + value: String, + } + impl Validate for ParentWithStructValidationsFirst { // Evaluating fields after their nested structs should fail because field // validations are expected to take priority over nested struct validations @@ -689,7 +853,7 @@ fn test_field_validations_evaluated_after_nested_validations_fails() { } // Then validate the length of the vector itself: - if !validate_length(&self.child, Some(2u64), None, None) { + if !self.child.validate_length(Some(2u64), None, None) { let mut err = ValidationError::new("length"); err.add_param(Cow::from("min"), &2u64); err.add_param(Cow::from("value"), &&self.child); @@ -706,6 +870,7 @@ fn test_field_validations_evaluated_after_nested_validations_fails() { let res = instance.validate(); } +#[allow(dead_code)] fn unwrap_map(errors: &ValidationErrors, f: F) where F: FnOnce(HashMap<&'static str, ValidationErrorsKind>), diff --git a/validator_derive_tests/tests/non_control.rs b/validator_derive_tests/tests/non_control.rs index 2ae78fcb..c4152ffc 100644 --- a/validator_derive_tests/tests/non_control.rs +++ b/validator_derive_tests/tests/non_control.rs @@ -80,8 +80,8 @@ fn can_validate_custom_impl_for_non_control_character() { val: CustomNonControlCharacter, } - impl validator::ValidateNonControlCharacter for &CustomNonControlCharacter { - fn to_non_control_character_iterator(&self) -> Box + '_> { + impl validator::ValidateNonControlCharacter for CustomNonControlCharacter { + fn as_non_control_character_iterator(&self) -> Box + '_> { Box::new(std::iter::once(self.char)) } } diff --git a/validator_derive_tests/tests/range.rs b/validator_derive_tests/tests/range.rs index f3ef9fa0..e7bd69f9 100644 --- a/validator_derive_tests/tests/range.rs +++ b/validator_derive_tests/tests/range.rs @@ -234,3 +234,31 @@ fn can_pass_reference_as_validate() { validate(&val).unwrap_err(); assert_eq!(val.num_field, 10); } + +#[test] +fn can_validate_option() { + #[derive(Validate)] + struct TestStruct { + #[validate(range(min = 100))] + num_field: Option, + #[validate(range(min = 5, exclusive_max = 10))] + exl_field: Option>, + } + + let t = TestStruct { num_field: Some(101), exl_field: Some(Some(9)) }; + assert!(t.validate().is_ok()); +} + +#[test] +fn can_validate_none_values() { + #[derive(Validate)] + struct TestStruct { + #[validate(range(min = 100))] + num_field: Option, + #[validate(range(min = 5, exclusive_max = 10))] + exl_field: Option>, + } + + let t = TestStruct { num_field: None, exl_field: None }; + assert!(t.validate().is_ok()); +} diff --git a/validator_derive_tests/tests/regex.rs b/validator_derive_tests/tests/regex.rs index 1768ee47..04a2777e 100644 --- a/validator_derive_tests/tests/regex.rs +++ b/validator_derive_tests/tests/regex.rs @@ -1,3 +1,6 @@ +use std::cell::OnceCell; +use std::sync::{Mutex, OnceLock}; + use lazy_static::lazy_static; use regex::Regex; use validator::Validate; @@ -6,11 +9,15 @@ lazy_static! { static ref RE2: Regex = Regex::new(r"^[a-z]{2}$").unwrap(); } +static REGEX_ONCE_LOCK: OnceLock = OnceLock::new(); +static REGEX_MUTEX_ONCE_CELL: Mutex> = Mutex::new(OnceCell::new()); +static REGEX_MUTEX_ONCE_LOCK: Mutex> = Mutex::new(OnceLock::new()); + #[test] fn can_validate_valid_regex() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(regex = "crate::RE2")] + #[validate(regex(path = *crate::RE2))] val: String, } @@ -23,7 +30,7 @@ fn can_validate_valid_regex() { fn bad_value_for_regex_fails_validation() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(regex = "crate::RE2")] + #[validate(regex(path = *crate::RE2))] val: String, } @@ -42,7 +49,7 @@ fn bad_value_for_regex_fails_validation() { fn can_specify_code_for_regex() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(regex(path = "crate::RE2", code = "oops"))] + #[validate(regex(path = *crate::RE2, code = "oops"))] val: String, } let s = TestStruct { val: "2".to_string() }; @@ -59,7 +66,7 @@ fn can_specify_code_for_regex() { fn can_specify_message_for_regex() { #[derive(Debug, Validate)] struct TestStruct { - #[validate(regex(path = "crate::RE2", message = "oops"))] + #[validate(regex(path = *crate::RE2, message = "oops"))] val: String, } let s = TestStruct { val: "2".to_string() }; @@ -71,3 +78,51 @@ fn can_specify_message_for_regex() { assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); } + +#[test] +fn can_specify_once_lock_for_regex() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(regex(path = crate::REGEX_ONCE_LOCK))] + val: String, + } + + let _ = REGEX_ONCE_LOCK.set(Regex::new(r"^[a-z]{2}$").unwrap()); + let t = TestStruct { val: "aa".to_string() }; + assert!(t.validate().is_ok()); + + let t = TestStruct { val: "aaa".to_string() }; + assert!(t.validate().is_err()); +} + +#[test] +fn can_specify_mutex_once_cell_for_regex() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(regex(path = crate::REGEX_MUTEX_ONCE_CELL))] + val: String, + } + + let _ = REGEX_MUTEX_ONCE_CELL.lock().unwrap().set(Regex::new(r"^[a-z]{2}$").unwrap()); + let t = TestStruct { val: "aa".to_string() }; + assert!(t.validate().is_ok()); + + let t = TestStruct { val: "aaa".to_string() }; + assert!(t.validate().is_err()); +} + +#[test] +fn can_specify_mutex_once_lock_for_regex() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(regex(path = crate::REGEX_MUTEX_ONCE_LOCK))] + val: String, + } + + let _ = REGEX_MUTEX_ONCE_LOCK.lock().unwrap().set(Regex::new(r"^[a-z]{2}$").unwrap()); + let t = TestStruct { val: "aa".to_string() }; + assert!(t.validate().is_ok()); + + let t = TestStruct { val: "aaa".to_string() }; + assert!(t.validate().is_err()); +} diff --git a/validator_derive_tests/tests/run-pass/absolute_path.rs b/validator_derive_tests/tests/run-pass/absolute_path.rs index c17e7760..82a0cdb5 100644 --- a/validator_derive_tests/tests/run-pass/absolute_path.rs +++ b/validator_derive_tests/tests/run-pass/absolute_path.rs @@ -1,16 +1,8 @@ use validator::Validate; -#[derive( - Clone, - Debug, - Eq, - PartialEq, - ::serde::Serialize, - ::serde::Deserialize, - Validate, -)] +#[derive(Clone, Debug, Eq, PartialEq, ::serde::Serialize, ::serde::Deserialize, Validate)] pub struct Message { - #[validate(length(min = 1i64, max = 2048i64))] + #[validate(length(min = 1u64, max = 2048u64))] pub text: String, } diff --git a/validator_derive_tests/tests/run-pass/custom.rs b/validator_derive_tests/tests/run-pass/custom.rs index 419bc0ed..237e3903 100644 --- a/validator_derive_tests/tests/run-pass/custom.rs +++ b/validator_derive_tests/tests/run-pass/custom.rs @@ -2,13 +2,13 @@ use validator::{Validate, ValidationError}; #[derive(Validate)] struct Test { - #[validate(custom = "crate::validate_something")] + #[validate(custom(function = crate::validate_something))] s: String, } #[derive(Validate)] struct TestPath { - #[validate(custom = "crate::validate_something")] + #[validate(custom(function = "crate::validate_something"))] s: String, } diff --git a/validator_derive_tests/tests/run-pass/must_match.rs b/validator_derive_tests/tests/run-pass/must_match.rs index 720fa0fa..5649b4c1 100644 --- a/validator_derive_tests/tests/run-pass/must_match.rs +++ b/validator_derive_tests/tests/run-pass/must_match.rs @@ -2,11 +2,11 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(must_match = "s2")] + #[validate(must_match(other = s2))] s: String, s2: String, - #[validate(must_match = "s4")] + #[validate(must_match(other = "s4"))] s3: usize, s4: usize, } diff --git a/validator_derive_tests/tests/run-pass/range.rs b/validator_derive_tests/tests/run-pass/range.rs index bff78551..31b46074 100644 --- a/validator_derive_tests/tests/run-pass/range.rs +++ b/validator_derive_tests/tests/run-pass/range.rs @@ -2,7 +2,7 @@ use validator::Validate; #[derive(Validate)] struct Test { - #[validate(range(min = 1, max = 2.2))] + #[validate(range(min = 1, max = 2))] s: isize, #[validate(range(min = 1, max = 2))] s2: usize, @@ -14,21 +14,21 @@ struct Test { s5: u32, #[validate(range(min = 18, max = 22))] s6: u64, - #[validate(range(min = 18.1, max = 22))] + #[validate(range(min = 18, max = 22))] s7: i8, - #[validate(range(min = 18.0, max = 22))] + #[validate(range(min = 18, max = 22))] s8: u8, - #[validate(range(min = 18.0, max = 22))] + #[validate(range(min = 18, max = 22))] s9: Option, - #[validate(range(min = 18.0))] + #[validate(range(min = 18))] s10: Option, - #[validate(range(max = 18.0))] + #[validate(range(max = 18))] s11: Option, } #[derive(Validate)] struct Test2 { - #[validate(range(exclusive_min = 1.1, max = 2.2))] + #[validate(range(exclusive_min = 1, max = 2))] s: isize, #[validate(range(min = 1, exclusive_max = 2))] s2: usize, @@ -40,15 +40,15 @@ struct Test2 { s5: u32, #[validate(range(min = 18, exclusive_max = 22))] s6: u64, - #[validate(range(exclusive_min = 18.1, max = 22))] + #[validate(range(exclusive_min = 18, max = 22))] s7: i8, - #[validate(range(exclusive_min = 18.0, max = 22))] + #[validate(range(exclusive_min = 18, max = 22))] s8: u8, - #[validate(range(exclusive_min = 18.0, exclusive_max = 22))] + #[validate(range(exclusive_min = 18, exclusive_max = 22))] s9: Option, - #[validate(range(exclusive_min = 18.0))] + #[validate(range(exclusive_min = 18))] s10: Option, - #[validate(range(exclusive_max = 18.0))] + #[validate(range(exclusive_max = 18))] s11: Option, } fn main() {} diff --git a/validator_derive_tests/tests/run-pass/regex.rs b/validator_derive_tests/tests/run-pass/regex.rs index 45dc47cc..f2ede5c8 100644 --- a/validator_derive_tests/tests/run-pass/regex.rs +++ b/validator_derive_tests/tests/run-pass/regex.rs @@ -8,13 +8,13 @@ lazy_static! { #[derive(Validate)] struct Test { - #[validate(regex = "crate::RE2")] + #[validate(regex(path = "*crate::RE2"))] s: String, } #[derive(Validate)] struct TestPath { - #[validate(regex = "crate::RE2")] + #[validate(regex(path = *crate::RE2))] s: String, } diff --git a/validator_derive_tests/tests/schema.rs b/validator_derive_tests/tests/schema.rs index a9815d2e..970bccd9 100644 --- a/validator_derive_tests/tests/schema.rs +++ b/validator_derive_tests/tests/schema.rs @@ -8,8 +8,8 @@ fn can_validate_schema_fn_ok() { #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "valid_schema_fn"))] - struct TestStruct { + #[validate(schema(function = valid_schema_fn))] + pub struct TestStruct { val: String, } @@ -22,13 +22,13 @@ mod some_defining_mod { use validator::Validate; #[derive(Debug, Validate)] - #[validate(schema(function = "crate::some_validation_mod::valid_schema_fn"))] + #[validate(schema(function = crate::some_validation_mod::valid_schema_fn))] pub struct TestStructValid { pub val: String, } #[derive(Debug, Validate)] - #[validate(schema(function = "crate::some_validation_mod::invalid_schema_fn"))] + #[validate(schema(function = crate::some_validation_mod::invalid_schema_fn))] pub struct TestStructInvalid { pub val: String, } @@ -80,8 +80,8 @@ fn can_validate_multiple_schema_fn_ok() { #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "valid_schema_fn"))] - #[validate(schema(function = "valid_schema_fn2"))] + #[validate(schema(function = valid_schema_fn))] + #[validate(schema(function = valid_schema_fn2))] struct TestStruct { val: String, } @@ -99,7 +99,7 @@ fn can_fail_schema_fn_validation() { #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "invalid_schema_fn"))] + #[validate(schema(function = invalid_schema_fn))] struct TestStruct { val: String, } @@ -126,8 +126,8 @@ fn can_fail_multiple_schema_fn_validation() { #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "invalid_schema_fn"))] - #[validate(schema(function = "invalid_schema_fn2"))] + #[validate(schema(function = invalid_schema_fn))] + #[validate(schema(function = invalid_schema_fn2))] struct TestStruct { val: String, } @@ -151,7 +151,7 @@ fn can_specify_message_for_schema_fn() { #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "invalid_schema_fn", message = "oops"))] + #[validate(schema(function = invalid_schema_fn, message = "oops"))] struct TestStruct { val: String, } @@ -172,7 +172,7 @@ fn can_choose_to_run_schema_validation_even_after_field_errors() { } #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "invalid_schema_fn", skip_on_field_errors = false))] + #[validate(schema(function = invalid_schema_fn, skip_on_field_errors = false))] struct TestStruct { val: String, #[validate(range(min = 1, max = 10))] @@ -192,3 +192,25 @@ fn can_choose_to_run_schema_validation_even_after_field_errors() { assert_eq!(errs["num"].len(), 1); assert_eq!(errs["num"][0].clone().code, "range"); } + +#[test] +fn schema_does_not_run_if_other_fields_have_errors() { + fn invalid_schema_fn(_: &TestStruct) -> Result<(), ValidationError> { + Err(ValidationError::new("meh")) + } + + #[allow(dead_code)] + #[derive(Debug, Validate)] + #[validate(schema(function = invalid_schema_fn))] + struct TestStruct { + #[validate(range(min = 1, max = 10))] + num: usize, + } + + let s = TestStruct { num: 0 }; + let res = s.validate(); + assert!(res.is_err()); + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(!errs.contains_key("__all__")); +} diff --git a/validator_derive_tests/tests/schema_args.rs b/validator_derive_tests/tests/schema_args.rs index 13f64429..e1a00821 100644 --- a/validator_derive_tests/tests/schema_args.rs +++ b/validator_derive_tests/tests/schema_args.rs @@ -18,65 +18,80 @@ fn validate_schema_fn_reference_with_lifetime_ok() { #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "valid_reference_with_lifetime", arg = "&'v_a mut CustomStruct"))] + #[validate(context = CustomStruct, mutable)] + #[validate(schema(function = valid_reference_with_lifetime, use_context))] struct TestStruct { value: String, } let s = TestStruct { value: "Hello World".to_string() }; let mut cs = CustomStruct { counter: 0 }; - assert!(s.validate_args(&mut cs).is_ok()); + assert!(s.validate_with_args(&mut cs).is_ok()); assert_eq!(cs.counter, 1); } #[test] -fn validate_schema_fn_tuple_err() { - fn invalid_custom_tuple(_: &TestStruct, _arg: (i64, i64)) -> Result<(), ValidationError> { +fn validate_schema_fn_err() { + fn invalid_custom_args(_: &TestStruct, _context: &Args) -> Result<(), ValidationError> { Err(ValidationError::new("meh")) } #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "invalid_custom_tuple", arg = "(i64, i64)"))] + #[validate(context = Args)] + #[validate(schema(function = invalid_custom_args, use_context))] struct TestStruct { value: String, } + #[allow(dead_code)] + struct Args { + arg1: i64, + arg2: i64, + } + let s = TestStruct { value: "Hello World".to_string() }; - assert!(s.validate_args((77, 555)).is_err()); + let args = Args { arg1: 1, arg2: 2 }; + assert!(s.validate_with_args(&args).is_err()); } #[test] fn invalidate_schema_fn_complex_arg_err() { - fn invalid_validation_complex_args<'a, T: AddAssign>( + fn invalid_validation_complex_args( _: &TestStruct, - arg: (&'a mut CustomStruct, &'a mut T, T), + args: &mut TestArgs<'_, i32>, ) -> Result<(), ValidationError> { - arg.0.counter += 1; - *arg.1 += arg.2; + args.arg1 += 1; + *args.arg3 += args.arg2; Err(ValidationError::new("meh")) } #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema( - function = "invalid_validation_complex_args", - arg = "(&'v_a mut CustomStruct, &'v_a mut i32, i32)" - ))] + #[validate(context = "TestArgs<'v_a, i32>", mutable)] + #[validate(schema(function = invalid_validation_complex_args, use_context))] struct TestStruct { value: String, } + struct TestArgs<'a, T: AddAssign> { + arg1: T, + arg2: &'a T, + arg3: &'a mut T, + } + let s = TestStruct { value: "Hello World".to_string() }; - let mut cs = CustomStruct { counter: 0 }; - let mut value = 10; - assert!(s.validate_args((&mut cs, &mut value, 5)).is_err()); - assert_eq!(cs.counter, 1); - assert_eq!(value, 15); + let mut args = TestArgs { arg1: 1, arg2: &2, arg3: &mut 3 }; + assert!(s.validate_with_args(&mut args).is_err()); } #[test] fn validate_schema_multiple_fn_with_args_ok() { + fn multiple_fn(t: &TestStruct, arg: &mut CustomStruct) -> Result<(), ValidationError> { + valid_reference_with_lifetime(t, arg)?; + invalid_custom_tuple(t, arg) + } + fn valid_reference_with_lifetime( _: &TestStruct, arg: &mut CustomStruct, @@ -85,14 +100,17 @@ fn validate_schema_multiple_fn_with_args_ok() { Ok(()) } - fn invalid_custom_tuple(_: &TestStruct, _arg: (i64, i64)) -> Result<(), ValidationError> { + fn invalid_custom_tuple( + _: &TestStruct, + _arg: &mut CustomStruct, + ) -> Result<(), ValidationError> { Err(ValidationError::new("meh")) } #[allow(dead_code)] #[derive(Debug, Validate)] - #[validate(schema(function = "valid_reference_with_lifetime", arg = "&'v_a mut CustomStruct"))] - #[validate(schema(function = "invalid_custom_tuple", arg = "(i64, i64)"))] + #[validate(context = CustomStruct, mutable)] + #[validate(schema(function = multiple_fn, use_context))] struct TestStruct { value: String, other_value: String, @@ -104,6 +122,6 @@ fn validate_schema_multiple_fn_with_args_ok() { }; let mut cs = CustomStruct { counter: 0 }; - assert!(s.validate_args((&mut cs, (123, 456))).is_err()); + assert!(s.validate_with_args(&mut cs).is_err()); assert_eq!(cs.counter, 1); } diff --git a/validator_derive_tests/tests/skip.rs b/validator_derive_tests/tests/skip.rs new file mode 100644 index 00000000..ee050567 --- /dev/null +++ b/validator_derive_tests/tests/skip.rs @@ -0,0 +1,40 @@ +use validator::Validate; + +#[test] +fn can_skip_field() { + #[derive(Validate)] + struct TestStruct { + #[validate(skip)] + #[validate(length(min = 5, max = 10))] + _skipped: String, + } + + let t = TestStruct { _skipped: "invalid value".to_string() }; + assert!(t.validate().is_ok()) +} + +#[test] +fn explicit_skip_field() { + #[derive(Validate)] + struct TestStruct { + #[validate(skip = true)] + #[validate(length(min = 5, max = 10))] + _skipped: String, + } + + let t = TestStruct { _skipped: "invalid value".to_string() }; + assert!(t.validate().is_ok()) +} + +#[test] +fn explicit_dont_skip_field() { + #[derive(Validate)] + struct TestStruct { + #[validate(skip = false)] + #[validate(length(min = 5, max = 10))] + _skipped: String, + } + + let t = TestStruct { _skipped: "invalid value".to_string() }; + assert!(t.validate().is_err()) +} diff --git a/validator_derive_tests/tests/unsupported_array.rs b/validator_derive_tests/tests/unsupported_array.rs index 7b0f5494..a6c21371 100644 --- a/validator_derive_tests/tests/unsupported_array.rs +++ b/validator_derive_tests/tests/unsupported_array.rs @@ -28,7 +28,7 @@ fn can_validate_custom_with_unsupported_array() { struct TestStruct { #[validate(email)] val: String, - #[validate(custom = "valid_custom_fn")] + #[validate(custom(function = valid_custom_fn))] array: [u8; 2], } @@ -43,7 +43,7 @@ fn can_fail_custom_with_unsupported_array() { struct TestStruct { #[validate(email)] val: String, - #[validate(custom = "valid_custom_fn")] + #[validate(custom(function = valid_custom_fn))] array: [u8; 2], } diff --git a/validator_derive_tests/tests/url.rs b/validator_derive_tests/tests/url.rs index fe8d4888..fc696a7c 100644 --- a/validator_derive_tests/tests/url.rs +++ b/validator_derive_tests/tests/url.rs @@ -87,12 +87,12 @@ fn can_validate_custom_impl_for_url() { val: CustomUrl, } - impl validator::ValidateUrl for &CustomUrl { - fn to_url_string(&self) -> Cow<'_, str> { - Cow::from(format!( + impl validator::ValidateUrl for CustomUrl { + fn as_url_string(&self) -> Option> { + Some(Cow::from(format!( "{}://{}.{}.{}", self.scheme, self.subdomain, self.domain, self.top_level_domain - )) + ))) } } diff --git a/validator_types/Cargo.toml b/validator_types/Cargo.toml deleted file mode 100644 index ed6c8523..00000000 --- a/validator_types/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "validator_types" -version = "0.16.0" -authors = ["Vincent Prouillet >, - }, - // String is the name of the field to match - MustMatch(String), - // value is a &str or a HashMap - Contains(String), - // No implementation in this crate, it's all in validator_derive - Regex(String), - Range { - min: Option>, - max: Option>, - exclusive_min: Option>, - exclusive_max: Option>, - }, - // Any value that impl HasLen can be validated with Length - Length { - min: Option>, - max: Option>, - equal: Option>, - }, - #[cfg(feature = "card")] - CreditCard, - Nested, - #[cfg(feature = "unic")] - NonControlCharacter, - Required, - RequiredNested, - DoesNotContain(String), -} - -#[derive(Debug, Clone, PartialEq)] -pub enum ValueOrPath { - Value(T), - Path(String), -} - -/// This struct stores information about defined custom arguments that will be passed in -/// by the user in the validation step. -#[derive(Debug, Clone)] -pub struct CustomArgument { - /// The span of type definition, this can be used in combination with `quote_spanned!` for - /// better error reporting - pub def_span: Span, - /// The type of the argument. This can use `'v_a` as a lifetime but has to be Sized. This - /// means that the type size has to be known at compile time - pub arg_type: Type, - /// This is the way we can access the value from the provided arguments. This will usually - /// look something like `args.0`. - pub arg_access: Option, -} - -impl CustomArgument { - pub fn new(def_span: Span, arg_type: Type) -> Self { - CustomArgument { def_span, arg_type, arg_access: None } - } -} - -impl Validator { - pub fn code(&self) -> &'static str { - match *self { - Validator::MustMatch(_) => "must_match", - Validator::Email => "email", - Validator::Url => "url", - Validator::Custom { .. } => "custom", - Validator::Contains(_) => "contains", - Validator::Regex(_) => "regex", - Validator::Range { .. } => "range", - Validator::Length { .. } => "length", - #[cfg(feature = "card")] - Validator::CreditCard => "credit_card", - Validator::Nested => "nested", - #[cfg(feature = "unic")] - Validator::NonControlCharacter => "non_control_character", - Validator::Required => "required", - Validator::RequiredNested => "required_nested", - Validator::DoesNotContain(_) => "does_not_contain", - } - } - - /// This returns the defined custom argument if it was defined - pub fn get_custom_argument(&self) -> Option<&CustomArgument> { - match self { - Validator::Custom { argument, .. } => (**argument).as_ref(), - _ => None, - } - } - - /// This returns the defined custom argument if it was defined - pub fn get_custom_argument_mut(&mut self) -> Option<&mut CustomArgument> { - match self { - Validator::Custom { argument, .. } => (**argument).as_mut(), - _ => None, - } - } - - pub fn has_custom_argument(&self) -> bool { - self.get_custom_argument().is_some() - } -}