diff --git a/README.md b/README.md index 63a4012c..e90227a9 100644 --- a/README.md +++ b/README.md @@ -212,8 +212,10 @@ const MIN_CONSTANT: i32 = 0; ``` -### must_match -Tests whether the 2 fields are equal. `must_match` takes 1 string argument. It will error if the field +### must_match and must_not_match +`must_match` tests whether the 2 fields are equal +`must_not_match` tests whether the 2 fields are not equal +They take 1 string argument. It will error if the field mentioned is missing or has a different type than the field the attribute is on. Examples: @@ -222,6 +224,10 @@ Examples: #[validate(must_match(other = "password2"))] ``` +```rust +#[validate(must_not_match(other = "id"))] +``` + ### contains Tests whether the string contains the substring given or if a key is present in a hashmap. `contains` takes 1 string argument. diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 2cb6d108..124f98ec 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -42,6 +42,7 @@ //! | `length` | | //! | `range` | | //! | `must_match` | | +//! | `must_not_match` | | //! | `contains` | | //! | `does_not_contain` | | //! | `custom` | | @@ -73,6 +74,7 @@ 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::must_not_match::validate_must_not_match; pub use validation::non_control_character::ValidateNonControlCharacter; pub use validation::range::ValidateRange; pub use validation::regex::{AsRegex, ValidateRegex}; diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs index 44831d32..e95d4039 100644 --- a/validator/src/validation/mod.rs +++ b/validator/src/validation/mod.rs @@ -6,6 +6,7 @@ pub mod email; pub mod ip; pub mod length; pub mod must_match; +pub mod must_not_match; // pub mod nested; pub mod non_control_character; pub mod range; diff --git a/validator/src/validation/must_not_match.rs b/validator/src/validation/must_not_match.rs new file mode 100644 index 00000000..517f3d8d --- /dev/null +++ b/validator/src/validation/must_not_match.rs @@ -0,0 +1,63 @@ +/// Validates that the 2 given fields do not match. +/// Both fields are optionals +#[must_use] +pub fn validate_must_not_match(a: T, b: T) -> bool { + a != b +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use super::validate_must_not_match; + + #[test] + fn test_validate_must_not_match_strings_valid() { + assert!(validate_must_not_match("hey".to_string(), "ho".to_string())) + } + + #[test] + fn test_validate_must_not_match_cows_valid() { + let left: Cow<'static, str> = "hey".into(); + let right: Cow<'static, str> = String::from("ho").into(); + assert!(validate_must_not_match(left, right)) + } + + #[test] + fn test_validate_must_not_match_numbers() { + assert!(validate_must_not_match(2, 3)) + } + + #[test] + fn test_validate_must_not_match_numbers_false() { + assert_eq!(false, validate_must_not_match(2, 2)); + } + + #[test] + fn test_validate_must_not_match_numbers_option_false() { + assert_eq!(false, validate_must_not_match(Some(2), Some(2))); + } + + #[test] + fn test_validate_must_not_match_numbers_option_true() { + assert!(validate_must_not_match(Some(6), Some(7))); + } + + #[test] + fn test_validate_must_not_match_none_some() { + assert!(validate_must_not_match(None, Some(3))); + } + + #[test] + fn test_validate_must_not_match_some_none() { + assert!(validate_must_not_match(Some(3), None)); + } + + #[test] + fn test_validate_must_not_match_none_none_false() { + // We need to define one of the values here as rust + // can not infer the generic type from None and None + let a: Option = None; + assert_eq!(false, validate_must_not_match(a, None)); + } +} \ No newline at end of file diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index 95e85508..1804afea 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -13,6 +13,7 @@ use tokens::email::email_tokens; use tokens::ip::ip_tokens; use tokens::length::length_tokens; use tokens::must_match::must_match_tokens; +use tokens::must_not_match::must_not_match_tokens; use tokens::nested::nested_tokens; use tokens::non_control_character::non_control_char_tokens; use tokens::range::range_tokens; @@ -178,6 +179,18 @@ impl ToTokens for ValidateField { quote!() }; + // Must not match validation + let must_not_match = if let Some(must_not_match) = self.must_not_match.clone() { + wrapper_closure(must_not_match_tokens( + &self.crate_name, + must_not_match, + &actual_field, + &field_name_str, + )) + } else { + quote!() + }; + // Regex validation let regex = if let Some(regex) = self.regex.clone() { wrapper_closure(regex_tokens(&self.crate_name, regex, &actual_field, &field_name_str)) @@ -231,6 +244,7 @@ impl ToTokens for ValidateField { #contains #does_not_contain #must_match + #must_not_match #regex #custom #nested diff --git a/validator_derive/src/tokens/mod.rs b/validator_derive/src/tokens/mod.rs index c3054bb4..f2583e45 100644 --- a/validator_derive/src/tokens/mod.rs +++ b/validator_derive/src/tokens/mod.rs @@ -6,6 +6,7 @@ pub mod email; pub mod ip; pub mod length; pub mod must_match; +pub mod must_not_match; pub mod nested; pub mod non_control_character; pub mod range; diff --git a/validator_derive/src/tokens/must_not_match.rs b/validator_derive/src/tokens/must_not_match.rs new file mode 100644 index 00000000..97d91882 --- /dev/null +++ b/validator_derive/src/tokens/must_not_match.rs @@ -0,0 +1,28 @@ +use quote::quote; + +use crate::types::MustNotMatch; +use crate::utils::{quote_code, quote_message, CrateName}; + +pub fn must_not_match_tokens( + crate_name: &CrateName, + must_not_match: MustNotMatch, + field_name: &proc_macro2::TokenStream, + field_name_str: &str, +) -> proc_macro2::TokenStream { + let o = must_not_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_not_match.message); + let code = quote_code(crate_name, must_not_match.code, "must_not_match"); + + quote! { + if !#crate_name::validate_must_not_match(&#field_name, &#other) { + #code + #message + #other_err + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + } +} diff --git a/validator_derive/src/types.rs b/validator_derive/src/types.rs index 8832fca2..927d6f23 100644 --- a/validator_derive/src/types.rs +++ b/validator_derive/src/types.rs @@ -58,6 +58,7 @@ pub struct ValidateField { pub ip: Option>, pub length: Option, pub must_match: Option, + pub must_not_match: Option, pub non_control_character: Option>, pub range: Option, pub required: Option>, @@ -276,6 +277,13 @@ pub struct MustMatch { pub code: Option, } +#[derive(Debug, Clone, FromMeta)] +pub struct MustNotMatch { + pub other: Path, + pub message: Option, + pub code: Option, +} + #[derive(Debug, Clone, FromMeta, Default)] pub struct NonControlCharacter { pub message: Option, diff --git a/validator_derive_tests/tests/compile-fail/must_not_match/field_doesnt_exist.rs b/validator_derive_tests/tests/compile-fail/must_not_match/field_doesnt_exist.rs new file mode 100644 index 00000000..e2dc974d --- /dev/null +++ b/validator_derive_tests/tests/compile-fail/must_not_match/field_doesnt_exist.rs @@ -0,0 +1,9 @@ +use validator::Validate; + +#[derive(Validate)] +struct Test { + #[validate(must_not_match(other = password2))] + password: String, +} + +fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/must_not_match/field_doesnt_exist.stderr b/validator_derive_tests/tests/compile-fail/must_not_match/field_doesnt_exist.stderr new file mode 100644 index 00000000..57ead5be --- /dev/null +++ b/validator_derive_tests/tests/compile-fail/must_not_match/field_doesnt_exist.stderr @@ -0,0 +1,10 @@ +error[E0609]: no field `password2` on type `&Test` + --> tests/compile-fail/must_not_match/field_doesnt_exist.rs:5:39 + | +5 | #[validate(must_not_match(other = password2))] + | ^^^^^^^^^ unknown field + | +help: a field with a similar name exists + | +5 | #[validate(must_not_match(other = password))] + | ~~~~~~~~ diff --git a/validator_derive_tests/tests/compile-fail/must_not_match/field_type_doesnt_match.rs b/validator_derive_tests/tests/compile-fail/must_not_match/field_type_doesnt_match.rs new file mode 100644 index 00000000..a93d8f42 --- /dev/null +++ b/validator_derive_tests/tests/compile-fail/must_not_match/field_type_doesnt_match.rs @@ -0,0 +1,10 @@ +use validator::Validate; + +#[derive(Validate)] +struct Test { + #[validate(must_not_match(other = "password2"))] + password: String, + password2: i32, +} + +fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/must_not_match/field_type_doesnt_match.stderr b/validator_derive_tests/tests/compile-fail/must_not_match/field_type_doesnt_match.stderr new file mode 100644 index 00000000..8d4840dc --- /dev/null +++ b/validator_derive_tests/tests/compile-fail/must_not_match/field_type_doesnt_match.stderr @@ -0,0 +1,17 @@ +error[E0308]: mismatched types + --> tests/compile-fail/must_not_match/field_type_doesnt_match.rs:3:10 + | +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_not_match.rs + | + | pub fn validate_must_not_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_not_match/unexpected_name_value.rs b/validator_derive_tests/tests/compile-fail/must_not_match/unexpected_name_value.rs new file mode 100644 index 00000000..4ab51c5c --- /dev/null +++ b/validator_derive_tests/tests/compile-fail/must_not_match/unexpected_name_value.rs @@ -0,0 +1,9 @@ +use validator::Validate; + +#[derive(Validate)] +struct Email { + #[validate(not_a(other = "validator"))] + email: String, +} + +fn main() {} diff --git a/validator_derive_tests/tests/compile-fail/must_not_match/unexpected_name_value.stderr b/validator_derive_tests/tests/compile-fail/must_not_match/unexpected_name_value.stderr new file mode 100644 index 00000000..1aadd33c --- /dev/null +++ b/validator_derive_tests/tests/compile-fail/must_not_match/unexpected_name_value.stderr @@ -0,0 +1,5 @@ +error: Unknown field: `not_a` + --> tests/compile-fail/must_not_match/unexpected_name_value.rs:5:16 + | +5 | #[validate(not_a(other = "validator"))] + | ^^^^^ diff --git a/validator_derive_tests/tests/must_not_match.rs b/validator_derive_tests/tests/must_not_match.rs new file mode 100644 index 00000000..ff365014 --- /dev/null +++ b/validator_derive_tests/tests/must_not_match.rs @@ -0,0 +1,73 @@ +use validator::Validate; + +#[test] +fn can_validate_valid_must_not_match() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_not_match(other = "val2"))] + val: String, + val2: String, + } + + let s = TestStruct { val: "bob".to_string(), val2: "alice".to_string() }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn matching_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_not_match(other = "val2"))] + val: String, + val2: String, + } + + let s = TestStruct { val: "bob".to_string(), val2: "bob".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, "must_not_match"); + assert_eq!(errs["val"][0].params["value"], "bob"); + assert_eq!(errs["val"][0].params["other"], "bob"); +} + +#[test] +fn can_specify_code_for_must_not_match() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_not_match(other = "val2", code = "oops"))] + val: String, + val2: String, + } + let s = TestStruct { val: "bob".to_string(), val2: "bob".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_must_not_match() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_not_match(other = "val2", message = "oops"))] + val: String, + val2: String, + } + let s = TestStruct { val: "bob".to_string(), val2: "bob".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"); +} diff --git a/validator_derive_tests/tests/run-pass/must_not_match.rs b/validator_derive_tests/tests/run-pass/must_not_match.rs new file mode 100644 index 00000000..70ec7ed8 --- /dev/null +++ b/validator_derive_tests/tests/run-pass/must_not_match.rs @@ -0,0 +1,14 @@ +use validator::Validate; + +#[derive(Validate)] +struct Test { + #[validate(must_not_match(other = s2))] + s: String, + s2: String, + + #[validate(must_not_match(other = "s4"))] + s3: usize, + s4: usize, +} + +fn main() {}