aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--README.md23
-rw-r--r--validator/Cargo.toml5
-rw-r--r--validator/src/lib.rs6
-rw-r--r--validator/src/validation/cards.rs24
-rw-r--r--validator/src/validation/mod.rs9
-rw-r--r--validator/src/validation/phone.rs33
-rw-r--r--validator_derive/src/lib.rs8
-rw-r--r--validator_derive/src/quoting.rs17
-rw-r--r--validator_derive/src/validation.rs9
-rw-r--r--validator_derive/tests/complex.rs10
-rw-r--r--validator_derive/tests/phone.rs77
-rw-r--r--validator_derive/tests/run-pass/phone.rs13
13 files changed, 227 insertions, 9 deletions
diff --git a/.travis.yml b/.travis.yml
index e1320c9..7f04152 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,7 @@ rust:
- nightly
script:
- - (cd validator && cargo test)
+ - (cd validator && cargo test --all-features)
- (cd validator_derive && cargo test)
notifications:
email: false
diff --git a/README.md b/README.md
index 3a8aa0c..1774902 100644
--- a/README.md
+++ b/README.md
@@ -6,17 +6,17 @@ Macros 1.1 custom derive to simplify struct validation inspired by [marshmallow]
[Django validators](https://docs.djangoproject.com/en/1.10/ref/validators/).
It relies on the `proc_macro` feature which is stable since Rust 1.15.
-By default all args to a `validate` must be strings if you are using stable.
+By default all args to a `validate` must be strings if you are using stable.
However, if you are using nightly, you can also activate the `attr_literals` feature to be able to use int, float and boolean as well.
A short example:
```rust
-#[macro_use]
+#[macro_use]
extern crate validator_derive;
extern crate validator;
-#[macro_use]
+#[macro_use]
extern crate serde_derive;
extern crate serde_json;
@@ -27,6 +27,8 @@ use validator::{Validate, ValidationError};
struct SignupData {
#[validate(email)]
mail: String,
+ #[validate(phone)]
+ phone: String,
#[validate(url)]
site: String,
#[validate(length(min = "1"), custom = "validate_unique_username")]
@@ -151,8 +153,21 @@ Examples:
#[validate(regex = "ALLOWED_USERNAMES_RE"))]
```
+### credit\_card
+Test whetever the string is a valid credit card number.
+
+Examples:
+
+```rust
+#[validate(credit_card)]
+```
+
+### phone
+Tests whether the String is a valid phone number (in international format, ie. containing the country indicator like `+14152370800` for an US number — where `4152370800` is the national number equivalent, which is seen as invalid). To use this validator, you must enable the `phone` feature for the `validator` crate.
+This validator doesn't take any arguments: `#[validate(phone)]`;
+
### custom
-Calls one of your function to do a custom validation.
+Calls one of your function to do a custom validation.
The field will be given as parameter and it should return a `Option<String>` representing the error code,
if there was an error.
diff --git a/validator/Cargo.toml b/validator/Cargo.toml
index 7cad55b..987f3b5 100644
--- a/validator/Cargo.toml
+++ b/validator/Cargo.toml
@@ -16,3 +16,8 @@ idna = "0.1"
serde = "1"
serde_derive = "1"
serde_json = "1"
+card-validate = "2.1"
+phonenumber = { version = "0.1.0+8.7.0", optional = true }
+
+[features]
+phone = ["phonenumber"]
diff --git a/validator/src/lib.rs b/validator/src/lib.rs
index 35afbe4..2cea387 100644
--- a/validator/src/lib.rs
+++ b/validator/src/lib.rs
@@ -7,6 +7,9 @@ extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;
+extern crate card_validate;
+#[cfg(feature = "phone")]
+pub extern crate phonenumber;
mod types;
mod validation;
@@ -19,6 +22,9 @@ pub use validation::range::{validate_range};
pub use validation::urls::{validate_url};
pub use validation::must_match::{validate_must_match};
pub use validation::contains::{validate_contains};
+pub use validation::cards::{validate_credit_card};
+#[cfg(feature = "phone")]
+pub use validation::phone::{validate_phone};
pub use validation::Validator;
pub use types::{ValidationErrors, ValidationError};
diff --git a/validator/src/validation/cards.rs b/validator/src/validation/cards.rs
new file mode 100644
index 0000000..b72663d
--- /dev/null
+++ b/validator/src/validation/cards.rs
@@ -0,0 +1,24 @@
+use card_validate::{Validate as CardValidate};
+
+
+pub fn validate_credit_card(card: &str) -> bool {
+ CardValidate::from(card).is_ok()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::validate_credit_card;
+ #[test]
+ fn test_credit_card() {
+ let tests = vec![
+ ("4539571147647251", true),
+ ("343380440754432", true),
+ ("zduhefljsdfKJKJZHUI", false),
+ ("5236313877109141", false),
+ ];
+
+ for (input, expected) in tests {
+ assert_eq!(validate_credit_card(input), expected);
+ }
+ }
+}
diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs
index 939cacf..6a21322 100644
--- a/validator/src/validation/mod.rs
+++ b/validator/src/validation/mod.rs
@@ -5,6 +5,9 @@ pub mod range;
pub mod urls;
pub mod must_match;
pub mod contains;
+pub mod cards;
+#[cfg(feature = "phone")]
+pub mod phone;
/// Contains all the validators that can be used
///
@@ -32,6 +35,9 @@ pub enum Validator {
max: Option<u64>,
equal: Option<u64>,
},
+ CreditCard(String),
+ #[cfg(feature = "phone")]
+ Phone,
}
impl Validator {
@@ -45,6 +51,9 @@ impl Validator {
Validator::Regex(_) => "regex",
Validator::Range {..} => "range",
Validator::Length {..} => "length",
+ Validator::CreditCard(_) => "credit_card",
+ #[cfg(feature = "phone")]
+ Validator::Phone => "phone",
}
}
}
diff --git a/validator/src/validation/phone.rs b/validator/src/validation/phone.rs
new file mode 100644
index 0000000..7772d61
--- /dev/null
+++ b/validator/src/validation/phone.rs
@@ -0,0 +1,33 @@
+use phonenumber;
+
+
+pub fn validate_phone(phone_number: &str) -> bool {
+ if let Ok(parsed) = phonenumber::parse(None, phone_number) {
+ phonenumber::is_valid(&parsed)
+ } else {
+ false
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::validate_phone;
+ #[test]
+ fn test_phone() {
+ let tests = vec![
+ ("+1 (415) 237-0800", true),
+ ("+14152370800", true),
+ ("+33642926829", true),
+ ("14152370800", false),
+ ("0642926829", false),
+ ("00642926829", false),
+ ("A012", false),
+ ("TEXT", false),
+ ];
+
+ for (input, expected) in tests {
+ println!("{} - {}", input, expected);
+ assert_eq!(validate_phone(input), expected);
+ }
+ }
+}
diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs
index 1298006..9cfc381 100644
--- a/validator_derive/src/lib.rs
+++ b/validator_derive/src/lib.rs
@@ -256,7 +256,7 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S
for meta_item in meta_items {
match *meta_item {
syn::NestedMetaItem::MetaItem(ref item) => match *item {
- // email, url
+ // email, url, phone
syn::MetaItem::Word(ref name) => match name.to_string().as_ref() {
"email" => {
assert_string_type("email", field_type);
@@ -266,6 +266,10 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S
assert_string_type("url", field_type);
validators.push(FieldValidation::new(Validator::Url));
},
+ "phone" => {
+ assert_string_type("phone", field_type);
+ validators.push(FieldValidation::new(Validator::Phone));
+ },
_ => panic!("Unexpected validator: {}", name)
},
// custom, contains, must_match, regex
@@ -311,7 +315,7 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S
assert_has_range(rust_ident.clone(), field_type);
validators.push(extract_range_validation(rust_ident.clone(), meta_items));
},
- "email" | "url" => {
+ "email" | "url" | "phone" => {
validators.push(extract_argless_validation(name.to_string(), rust_ident.clone(), meta_items));
},
"custom" => {
diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs
index 69353e9..24c50d8 100644
--- a/validator_derive/src/quoting.rs
+++ b/validator_derive/src/quoting.rs
@@ -158,6 +158,22 @@ pub fn quote_range_validation(field_quoter: &FieldQuoter, validation: &FieldVali
unreachable!()
}
+pub fn quote_phone_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens {
+ 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_phone(#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) -> quote::Tokens {
let field_name = &field_quoter.name;
let validator_param = field_quoter.quote_validator_param();
@@ -293,6 +309,7 @@ pub fn quote_field_validation(field_quoter: &FieldQuoter, validation: &FieldVali
Validator::Custom(_) => quote_custom_validation(&field_quoter, validation),
Validator::Contains(_) => quote_contains_validation(&field_quoter, validation),
Validator::Regex(_) => quote_regex_validation(&field_quoter, validation),
+ Validator::Phone => quote_phone_validation(&field_quoter, validation),
}
}
diff --git a/validator_derive/src/validation.rs b/validator_derive/src/validation.rs
index a3350da..259db70 100644
--- a/validator_derive/src/validation.rs
+++ b/validator_derive/src/validation.rs
@@ -173,7 +173,7 @@ pub fn extract_range_validation(field: String, meta_items: &Vec<syn::NestedMetaI
}
}
-/// Extract url/email field validation with a code or a message
+/// Extract url/email/phone field validation with a code or a message
pub fn extract_argless_validation(validator_name: String, field: String, meta_items: &Vec<syn::NestedMetaItem>) -> FieldValidation {
let mut code = None;
let mut message = None;
@@ -213,7 +213,12 @@ pub fn extract_argless_validation(validator_name: String, field: String, meta_it
}
}
- let validator = if validator_name == "email" { Validator::Email } else { Validator::Url };
+ let validator = match validator_name.as_ref() {
+ "email" => Validator::Email,
+ "phone" => Validator::Phone,
+ _ => Validator::Url
+ };
+
FieldValidation {
message,
code: code.unwrap_or_else(|| validator.code().to_string()),
diff --git a/validator_derive/tests/complex.rs b/validator_derive/tests/complex.rs
index db5f1bf..80ae360 100644
--- a/validator_derive/tests/complex.rs
+++ b/validator_derive/tests/complex.rs
@@ -33,6 +33,8 @@ fn validate_signup(data: &SignupData) -> Result<(), ValidationError> {
struct SignupData {
#[validate(email)]
mail: String,
+ #[validate(phone)]
+ phone: String,
#[validate(url)]
site: String,
#[validate(length(min = "1"), custom = "validate_unique_username")]
@@ -47,6 +49,7 @@ struct SignupData {
fn is_fine_with_many_valid_validations() {
let signup = SignupData {
mail: "bob@bob.com".to_string(),
+ phone: "+14152370800".to_string(),
site: "http://hello.com".to_string(),
first_name: "Bob".to_string(),
age: 18,
@@ -59,6 +62,7 @@ fn is_fine_with_many_valid_validations() {
fn failed_validation_points_to_original_field_name() {
let signup = SignupData {
mail: "bob@bob.com".to_string(),
+ phone: "+14152370800".to_string(),
site: "http://hello.com".to_string(),
first_name: "".to_string(),
age: 18,
@@ -85,6 +89,8 @@ fn test_can_validate_option_fields_with_lifetime() {
range: Option<usize>,
#[validate(email)]
email: Option<&'a str>,
+ #[validate(phone)]
+ phone: Option<&'a str>,
#[validate(url)]
url: Option<&'a str>,
#[validate(contains = "@")]
@@ -103,6 +109,7 @@ fn test_can_validate_option_fields_with_lifetime() {
name: Some("al"),
range: Some(2),
email: Some("hi@gmail.com"),
+ phone: Some("+14152370800"),
url: Some("http://google.com"),
text: Some("@someone"),
re: Some("hi"),
@@ -127,6 +134,8 @@ fn test_can_validate_option_fields_without_lifetime() {
range: Option<usize>,
#[validate(email)]
email: Option<String>,
+ #[validate(phone)]
+ phone: Option<String>,
#[validate(url)]
url: Option<String>,
#[validate(contains = "@")]
@@ -146,6 +155,7 @@ fn test_can_validate_option_fields_without_lifetime() {
ids: Some(vec![1, 2, 3]),
range: Some(2),
email: Some("hi@gmail.com".to_string()),
+ phone: Some("+14152370800".to_string()),
url: Some("http://google.com".to_string()),
text: Some("@someone".to_string()),
re: Some("hi".to_string()),
diff --git a/validator_derive/tests/phone.rs b/validator_derive/tests/phone.rs
new file mode 100644
index 0000000..675d4c5
--- /dev/null
+++ b/validator_derive/tests/phone.rs
@@ -0,0 +1,77 @@
+#[macro_use]
+extern crate validator_derive;
+extern crate validator;
+
+use validator::Validate;
+
+
+#[test]
+fn can_validate_phone_ok() {
+ #[derive(Debug, Validate)]
+ struct TestStruct {
+ #[validate(phone)]
+ val: String,
+ }
+
+ let s = TestStruct {
+ val: "+14152370800".to_string(),
+ };
+
+ assert!(s.validate().is_ok());
+}
+
+#[test]
+fn bad_phone_fails_validation() {
+ #[derive(Debug, Validate)]
+ struct TestStruct {
+ #[validate(phone)]
+ val: String,
+ }
+
+ let s = TestStruct {
+ val: "bob".to_string(),
+ };
+ let res = s.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err().inner();
+ assert!(errs.contains_key("val"));
+ assert_eq!(errs["val"].len(), 1);
+ assert_eq!(errs["val"][0].code, "phone");
+}
+
+#[test]
+fn can_specify_code_for_phone() {
+ #[derive(Debug, Validate)]
+ struct TestStruct {
+ #[validate(phone(code = "oops"))]
+ val: String,
+ }
+ let s = TestStruct {
+ val: "bob".to_string(),
+ };
+ let res = s.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err().inner();
+ 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"], "bob");
+}
+
+#[test]
+fn can_specify_message_for_phone() {
+ #[derive(Debug, Validate)]
+ struct TestStruct {
+ #[validate(phone(message = "oops"))]
+ val: String,
+ }
+ let s = TestStruct {
+ val: "bob".to_string(),
+ };
+ let res = s.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err().inner();
+ 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/run-pass/phone.rs b/validator_derive/tests/run-pass/phone.rs
new file mode 100644
index 0000000..d6dff9e
--- /dev/null
+++ b/validator_derive/tests/run-pass/phone.rs
@@ -0,0 +1,13 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+struct Test {
+ #[validate(phone)]
+ s: String,
+}
+
+fn main() {}