diff options
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | validator/Cargo.toml | 2 | ||||
| -rw-r--r-- | validator/src/lib.rs | 2 | ||||
| -rw-r--r-- | validator/src/validation/email.rs | 2 | ||||
| -rw-r--r-- | validator/src/validation/mod.rs | 6 | ||||
| -rw-r--r-- | validator/src/validation/non_control_character.rs | 46 | ||||
| -rw-r--r-- | validator_derive/Cargo.toml | 1 | ||||
| -rw-r--r-- | validator_derive/src/lib.rs | 9 | ||||
| -rw-r--r-- | validator_derive/src/quoting.rs | 24 | ||||
| -rw-r--r-- | validator_derive/src/validation.rs | 4 | ||||
| -rw-r--r-- | validator_derive/tests/non_control.rs | 75 |
11 files changed, 172 insertions, 4 deletions
@@ -255,6 +255,11 @@ Examples: #[validate] ``` +### non_control_character +Tests whether the String has any utf-8 control caracters, fails validation if it does. +To use this validator, you must enable the `unic` feature for the `validator_derive` crate. +This validator doesn't take any arguments: `#[validate(non_control_character)]`; + ## Struct level validation Often, some error validation can only be applied when looking at the full struct, here's how it works here: diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 96e8104..afc282b 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -19,7 +19,9 @@ serde_derive = "1" serde_json = "1" card-validate = { version = "2.1", optional = true } phonenumber = { version = "0.2.0+8.7.0", optional = true } +unic-ucd-common = { version = "0.9.0", optional = true } [features] phone = ["phonenumber"] card = ["card-validate"] +unic = ["unic-ucd-common"]
\ No newline at end of file diff --git a/validator/src/lib.rs b/validator/src/lib.rs index f6aa8f0..7839c2b 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -23,6 +23,8 @@ pub use validation::email::validate_email; pub use validation::ip::{validate_ip, validate_ip_v4, validate_ip_v6}; pub use validation::length::validate_length; pub use validation::must_match::validate_must_match; +#[cfg(feature = "unic")] +pub use validation::non_control_character::validate_non_control_character; #[cfg(feature = "phone")] pub use validation::phone::validate_phone; pub use validation::range::validate_range; diff --git a/validator/src/validation/email.rs b/validator/src/validation/email.rs index 6ea2fae..cd8c6d2 100644 --- a/validator/src/validation/email.rs +++ b/validator/src/validation/email.rs @@ -120,7 +120,7 @@ mod tests { ]; for (input, expected) in tests { - println!("{} - {}", input, expected); + // println!("{} - {}", input, expected); assert_eq!(validate_email(input), expected); } } diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs index 95d9b79..1675730 100644 --- a/validator/src/validation/mod.rs +++ b/validator/src/validation/mod.rs @@ -5,6 +5,8 @@ pub mod email; pub mod ip; pub mod length; pub mod must_match; +#[cfg(feature = "unic")] +pub mod non_control_character; #[cfg(feature = "phone")] pub mod phone; pub mod range; @@ -41,6 +43,8 @@ pub enum Validator { #[cfg(feature = "phone")] Phone, Nested, + #[cfg(feature = "unic")] + NonControlCharacter, } impl Validator { @@ -59,6 +63,8 @@ impl Validator { #[cfg(feature = "phone")] Validator::Phone => "phone", Validator::Nested => "nested", + #[cfg(feature = "unic")] + Validator::NonControlCharacter => "non_control_character", } } } diff --git a/validator/src/validation/non_control_character.rs b/validator/src/validation/non_control_character.rs new file mode 100644 index 0000000..ec4308c --- /dev/null +++ b/validator/src/validation/non_control_character.rs @@ -0,0 +1,46 @@ +use std::borrow::Cow; +use unic_ucd_common::control; + +pub fn validate_non_control_character<'a, T>(alphabetic: T) -> bool +where + T: Into<Cow<'a, str>> + Clone, +{ + alphabetic.into().chars().into_iter().all(|code| !control::is_control(code)) +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use super::validate_non_control_character; + + #[test] + fn test_non_control_character() { + let tests = vec![ + ("Himmel", true), + ("आकाश", true), + ("வானத்தில்", true), + ("하늘", true), + ("небо", true), + ("2H₂ + O₂ ⇌ 2H₂O", true), + ("\u{000c}", false), + ("\u{009F}", false), + ]; + + for (input, expected) in tests { + assert_eq!(validate_non_control_character(input), expected); + } + } + + #[test] + fn test_non_control_character_cow() { + let test: Cow<'static, str> = "आकाश".into(); + assert_eq!(validate_non_control_character(test), true); + let test: Cow<'static, str> = String::from("வானத்தில்").into(); + assert_eq!(validate_non_control_character(test), true); + let test: Cow<'static, str> = "\u{000c}".into(); + assert_eq!(validate_non_control_character(test), false); + let test: Cow<'static, str> = String::from("\u{009F}").into(); + assert_eq!(validate_non_control_character(test), false); + } +} diff --git a/validator_derive/Cargo.toml b/validator_derive/Cargo.toml index e8e50aa..5c515de 100644 --- a/validator_derive/Cargo.toml +++ b/validator_derive/Cargo.toml @@ -14,6 +14,7 @@ proc-macro = true [features] phone = ["validator/phone"] card = ["validator/card"] +unic = ["validator/unic"] [dependencies] syn = { version = "0.15", features = ["extra-traits"] } diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index f3796b4..d8bbb3e 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -266,7 +266,7 @@ fn find_validators_for_field( for meta_item in meta_items { match *meta_item { syn::NestedMeta::Meta(ref item) => match *item { - // email, url, phone + // email, url, phone, credit_card, non_control_character syn::Meta::Word(ref name) => match name.to_string().as_ref() { "email" => { assert_string_type("email", field_type); @@ -286,6 +286,11 @@ fn find_validators_for_field( assert_string_type("credit_card", field_type); validators.push(FieldValidation::new(Validator::CreditCard)); } + #[cfg(feature = "unic")] + "non_control_character" => { + assert_string_type("non_control_character", field_type); + validators.push(FieldValidation::new(Validator::NonControlCharacter)); + } _ => panic!("Unexpected validator: {}", name), }, // custom, contains, must_match, regex @@ -341,7 +346,7 @@ fn find_validators_for_field( &meta_items, )); } - "email" | "url" | "phone" | "credit_card" => { + "email" | "url" | "phone" | "credit_card" | "non_control_character" => { validators.push(extract_argless_validation( ident.to_string(), rust_ident.clone(), diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs index b94962c..6bfef0c 100644 --- a/validator_derive/src/quoting.rs +++ b/validator_derive/src/quoting.rs @@ -258,6 +258,26 @@ pub fn quote_phone_validation( 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, @@ -440,6 +460,10 @@ pub fn quote_field_validation( #[cfg(feature = "phone")] Validator::Phone => validations.push(quote_phone_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)) + } } } diff --git a/validator_derive/src/validation.rs b/validator_derive/src/validation.rs index 5f42d7b..be8afee 100644 --- a/validator_derive/src/validation.rs +++ b/validator_derive/src/validation.rs @@ -146,7 +146,7 @@ pub fn extract_range_validation( } } -/// Extract url/email/phone field validation with a code or a message +/// Extract url/email/phone/non_control_character field validation with a code or a message pub fn extract_argless_validation( validator_name: String, field: String, @@ -178,6 +178,8 @@ pub fn extract_argless_validation( "credit_card" => Validator::CreditCard, #[cfg(feature = "phone")] "phone" => Validator::Phone, + #[cfg(feature = "unic")] + "non_control_character" => Validator::NonControlCharacter, _ => Validator::Url, }; diff --git a/validator_derive/tests/non_control.rs b/validator_derive/tests/non_control.rs new file mode 100644 index 0000000..37fb153 --- /dev/null +++ b/validator_derive/tests/non_control.rs @@ -0,0 +1,75 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; + +#[cfg(feature = "unic")] +#[test] +fn can_validate_utf8_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(non_control_character)] + val: String, + } + + let s = TestStruct { val: "하늘".to_string() }; + + assert!(s.validate().is_ok()); +} + +#[cfg(feature = "unic")] +#[test] +fn utf8_with_control_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(non_control_character)] + val: String, + } + + let s = TestStruct { val: "\u{009F}하늘".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, "non_control_character"); +} + +#[cfg(feature = "unic")] +#[test] +fn can_specify_code_for_non_control_character() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(non_control_character(code = "oops"))] + val: String, + } + let s = TestStruct { val: "\u{009F}하늘".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"); + assert_eq!(errs["val"][0].params["value"], "\u{9F}하늘"); +} + +#[cfg(feature = "unic")] +#[test] +fn can_specify_message_for_non_control_character() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(non_control_character(message = "oops"))] + val: String, + } + let s = TestStruct { val: "\u{009F}하늘".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"); +} |
