diff options
| -rw-r--r-- | .travis.yml | 2 | ||||
| -rw-r--r-- | README.md | 23 | ||||
| -rw-r--r-- | validator/Cargo.toml | 5 | ||||
| -rw-r--r-- | validator/src/lib.rs | 6 | ||||
| -rw-r--r-- | validator/src/validation/cards.rs | 24 | ||||
| -rw-r--r-- | validator/src/validation/mod.rs | 9 | ||||
| -rw-r--r-- | validator/src/validation/phone.rs | 33 | ||||
| -rw-r--r-- | validator_derive/src/lib.rs | 8 | ||||
| -rw-r--r-- | validator_derive/src/quoting.rs | 17 | ||||
| -rw-r--r-- | validator_derive/src/validation.rs | 9 | ||||
| -rw-r--r-- | validator_derive/tests/complex.rs | 10 | ||||
| -rw-r--r-- | validator_derive/tests/phone.rs | 77 | ||||
| -rw-r--r-- | validator_derive/tests/run-pass/phone.rs | 13 | 
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 @@ -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() {} | 
