aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVincent Prouillet2019-10-14 19:53:18 +0200
committerGitHub2019-10-14 19:53:18 +0200
commit5c509da6e41726e5a80d52c10f70e582b28d145f (patch)
treeed2962ef987c124d9f44cb14c2dacdc7aaf97797
parent9554ba1116e2cbdf4ea565616c627792c1d03fa9 (diff)
parentac3810f68d2dd86c17a57593959506bd1b99cab5 (diff)
downloadvalidator-5c509da6e41726e5a80d52c10f70e582b28d145f.tar.bz2
Merge pull request #79 from mocsy/master
Add utf-8 non-control characters validator
-rw-r--r--README.md5
-rw-r--r--validator/Cargo.toml2
-rw-r--r--validator/src/lib.rs2
-rw-r--r--validator/src/validation/email.rs2
-rw-r--r--validator/src/validation/mod.rs6
-rw-r--r--validator/src/validation/non_control_character.rs46
-rw-r--r--validator_derive/Cargo.toml1
-rw-r--r--validator_derive/src/lib.rs9
-rw-r--r--validator_derive/src/quoting.rs24
-rw-r--r--validator_derive/src/validation.rs4
-rw-r--r--validator_derive/tests/non_control.rs75
11 files changed, 172 insertions, 4 deletions
diff --git a/README.md b/README.md
index 875f066..c422248 100644
--- a/README.md
+++ b/README.md
@@ -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");
+}