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"); +} | 
