From c5c76fac8726d53988092d71c818d38f12e8348e Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 27 Dec 2016 14:31:17 +0900 Subject: Initial commit --- .editorconfig | 5 + .gitignore | 4 + .travis.yml | 11 + LICENSE | 22 ++ README.md | 118 ++++++ validator/Cargo.toml | 15 + validator/src/email.rs | 121 ++++++ validator/src/ip.rs | 107 ++++++ validator/src/length.rs | 94 +++++ validator/src/lib.rs | 20 + validator/src/range.rs | 30 ++ validator/src/types.rs | 30 ++ validator/src/urls.rs | 30 ++ validator_derive/Cargo.toml | 25 ++ validator_derive/src/lib.rs | 410 +++++++++++++++++++++ .../tests/compile-fail/custom_not_string.rs | 15 + .../compile-fail/length/equal_and_min_max_set.rs | 15 + .../tests/compile-fail/length/no_args.rs | 15 + .../tests/compile-fail/length/unknown_arg.rs | 15 + .../tests/compile-fail/length/wrong_arg_type.rs | 15 + .../tests/compile-fail/length/wrong_type.rs | 15 + .../tests/compile-fail/no_validations.rs | 15 + .../tests/compile-fail/range/missing_arg.rs | 15 + .../tests/compile-fail/range/no_args.rs | 15 + .../tests/compile-fail/range/unknown_arg.rs | 15 + .../tests/compile-fail/range/wrong_arg_type.rs | 15 + .../tests/compile-fail/range/wrong_type.rs | 15 + validator_derive/tests/compile_test.rs | 24 ++ validator_derive/tests/run-pass/custom.rs | 17 + validator_derive/tests/run-pass/email.rs | 13 + validator_derive/tests/run-pass/length.rs | 28 ++ validator_derive/tests/run-pass/range.rs | 27 ++ validator_derive/tests/run-pass/url.rs | 13 + validator_derive/tests/test_derive.rs | 136 +++++++ 34 files changed, 1480 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 validator/Cargo.toml create mode 100644 validator/src/email.rs create mode 100644 validator/src/ip.rs create mode 100644 validator/src/length.rs create mode 100644 validator/src/lib.rs create mode 100644 validator/src/range.rs create mode 100644 validator/src/types.rs create mode 100644 validator/src/urls.rs create mode 100644 validator_derive/Cargo.toml create mode 100644 validator_derive/src/lib.rs create mode 100644 validator_derive/tests/compile-fail/custom_not_string.rs create mode 100644 validator_derive/tests/compile-fail/length/equal_and_min_max_set.rs create mode 100644 validator_derive/tests/compile-fail/length/no_args.rs create mode 100644 validator_derive/tests/compile-fail/length/unknown_arg.rs create mode 100644 validator_derive/tests/compile-fail/length/wrong_arg_type.rs create mode 100644 validator_derive/tests/compile-fail/length/wrong_type.rs create mode 100644 validator_derive/tests/compile-fail/no_validations.rs create mode 100644 validator_derive/tests/compile-fail/range/missing_arg.rs create mode 100644 validator_derive/tests/compile-fail/range/no_args.rs create mode 100644 validator_derive/tests/compile-fail/range/unknown_arg.rs create mode 100644 validator_derive/tests/compile-fail/range/wrong_arg_type.rs create mode 100644 validator_derive/tests/compile-fail/range/wrong_type.rs create mode 100644 validator_derive/tests/compile_test.rs create mode 100644 validator_derive/tests/run-pass/custom.rs create mode 100644 validator_derive/tests/run-pass/email.rs create mode 100644 validator_derive/tests/run-pass/length.rs create mode 100644 validator_derive/tests/run-pass/range.rs create mode 100644 validator_derive/tests/run-pass/url.rs create mode 100644 validator_derive/tests/test_derive.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..505b806 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +[*.rs] +end_of_line = lf +charset = utf-8 +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30ec920 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/*.iml +/.idea +target/ +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e1320c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: rust +cache: cargo + +rust: + - nightly + +script: + - (cd validator && cargo test) + - (cd validator_derive && cargo test) +notifications: + email: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1a4c480 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Vincent Prouillet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..05d74be --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# validator + +[![Build Status](https://travis-ci.org/Keats/validator.svg)](https://travis-ci.org/Keats/validator) + +Status: Experimental - do not use in production + +Macros 1.1 custom derive to simplify struct validation inspired by [marshmallow](http://marshmallow.readthedocs.io/en/latest/) and +[Django validators](https://docs.djangoproject.com/en/1.10/ref/validators/). +It currently only works on nightly but should work on stable in Rust 1.15. + +A short example: +```rust +#![feature(proc_macro, attr_literals)] + +#[macro_use] extern crate validator_derive; +extern crate validator; +#[macro_use] extern crate serde_derive; +extern crate serde_json; + +// A trait that the Validate derive will impl +use validator::Validate; + +#[derive(Debug, Validate, Deserialize)] +struct SignupData { + #[validate(email)] + mail: String, + #[validate(url)] + site: String, + #[validate(length(min = 1), custom = "validate_unique_username")] + #[serde(rename = "firstName")] + first_name: String, + #[validate(range(min = 18, max = 20))] + age: u32, +} + +fn validate_unique_username(username: &str) -> Option { + if username == "xXxShad0wxXx" { + return Some("terrible_username".to_string()); + } + + None +} + +// load the struct from some json... +// `validate` returns `Result<(), HashMap>>` +signup_data.validate()?; +``` + +This crate only sends back error codes for each field, it's up to you to write a message +for each error code. + +Note that `validator` works in conjunction with serde: in the example we can see that the `first_name` +field is renamed from/to `firstName`. Any error on that field will be in the `firstName` key of the hashmap, +not `first_name`. + +## Usage +You will need to add the `proc_macro` and `attr_literals` as seen in the example, as +well as importing the `Validate` trait. + +The `validator` crate can also be used without the custom derive as it exposes all the +validation functions. + +## Validators +The crate comes with some built-in validators and you can have several validators for a given field. + +### email +Tests whether the String is a valid email according to the HTML5 regex, which means it will mark +some esoteric emails as invalid that won't be valid in a `email` input as well. +This validator doesn't take any arguments: `#[validate(email)]`. + +### url +Tests whether the String is a valid URL. +This validator doesn't take any arguments: `#[validate(url)]`; + +### length +Tests whether a String or a Vec match the length requirement given. `length` has 3 integer arguments: + +- min +- max +- equal + +Using `equal` excludes the `min` or `max` and will result in a compilation error if they are found. + +At least one argument is required with a maximum of 2 (having `min` and `max` at the same time). + +Examples: + +```rust +#[validate(length(min = 1, max = 10))] +#[validate(length(min = 1))] +#[validate(length(max = 10))] +#[validate(length(equal = 10))] +``` + +### range +Tests whether a number is in the given range. `range` takes 2 number arguments: `min` and `max`. + +Examples: + +```rust +#[validate(range(min = 1, max = 10))] +#[validate(range(min = 1, max = 10.8))] +#[validate(range(min = 1.1, max = 10.8))] +``` + +### custom +Calls one of your function to do a custom validation. +The field will be given as parameter and it should return a `Option` representing the error code, +if there was an error. + +Examples: + +```rust +#[validate(custom = "validate_something")] +#[validate(custom = "::utils::validate_something"] +``` + +TODO: have it return a bool and pass a `code` to the `custom` validator instead? diff --git a/validator/Cargo.toml b/validator/Cargo.toml new file mode 100644 index 0000000..618c114 --- /dev/null +++ b/validator/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "validator" +version = "0.1.0" +authors = ["Vincent Prouillet "] +license = "MIT" +description = "Common validation functions (email, url, length, ...) and trait" +homepage = "https://github.com/Keats/validator" +repository = "https://github.com/Keats/validator" +keywords = ["validation", "api", "validator"] + +[dependencies] +url = "1.2" +regex = "0.1" +lazy_static = "0.2" +idna = "0.1" diff --git a/validator/src/email.rs b/validator/src/email.rs new file mode 100644 index 0000000..d7cd084 --- /dev/null +++ b/validator/src/email.rs @@ -0,0 +1,121 @@ +use regex::Regex; + +use ip::{validate_ip}; +use idna::{domain_to_ascii}; + + +lazy_static! { + // Regex from the specs + // https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address + // It will mark esoteric email addresses like quoted string as invalid + static ref EMAIL_USER_RE: Regex = Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap(); + static ref EMAIL_DOMAIN_RE: Regex = Regex::new( + r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$" + ).unwrap(); + // literal form, ipv4 or ipv6 address (SMTP 4.1.3) + static ref EMAIL_LITERAL_RE: Regex = Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap(); +} + +/// Validates whether the given string is an email based on Django `EmailValidator` and HTML5 specs +pub fn validate_email(val: &str) -> bool { + if val.is_empty() || !val.contains('@') { + return false; + } + let parts: Vec<&str> = val.rsplitn(2, '@').collect(); + let user_part = parts[1]; + let domain_part = parts[0]; + + if !EMAIL_USER_RE.is_match(user_part) { + return false; + } + + if !validate_domain_part(domain_part) { + // Still the possibility of an [IDN](https://en.wikipedia.org/wiki/Internationalized_domain_name) + return match domain_to_ascii(domain_part) { + Ok(d) => validate_domain_part(&d), + Err(_) => false, + }; + } + + true +} + +/// Checks if the domain is a valid domain and if not, check whether it's an IP +fn validate_domain_part(domain_part: &str) -> bool { + if EMAIL_DOMAIN_RE.is_match(domain_part) { + return true; + } + + // maybe we have an ip as a domain? + match EMAIL_LITERAL_RE.captures(domain_part) { + Some(caps) => { + match caps.at(1) { + Some(c) => validate_ip(c), + None => false, + } + } + None => false + } +} + + +#[cfg(test)] +mod tests { + use super::validate_email; + + #[test] + fn test_validate_email() { + // Test cases taken from Django + // https://github.com/django/django/blob/master/tests/validators/tests.py#L48 + let tests = vec![ + ("email@here.com", true), + ("weirder-email@here.and.there.com", true), + (r#"!def!xyz%abc@example.com"#, true), + ("email@[127.0.0.1]", true), + ("email@[2001:dB8::1]", true), + ("email@[2001:dB8:0:0:0:0:0:1]", true), + ("email@[::fffF:127.0.0.1]", true), + ("example@valid-----hyphens.com", true), + ("example@valid-with-hyphens.com", true), + ("test@domain.with.idn.tld.उदाहरण.परीक्षा", true), + (r#""test@test"@example.com"#, false), + // max length for domain name labels is 63 characters per RFC 1034 + ("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true), + ("a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.atm", true), + ("a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbb.atm", true), + ("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true), + ("", false), + ("abc", false), + ("abc@", false), + ("abc@bar", true), + ("a @x.cz", false), + // TODO: make that one below fail + // ("abc@.com", false), + ("something@@somewhere.com", false), + ("email@127.0.0.1", true), + ("email@[127.0.0.256]", false), + ("email@[2001:db8::12345]", false), + ("email@[2001:db8:0:0:0:0:1]", false), + ("email@[::ffff:127.0.0.256]", false), + ("example@invalid-.com", false), + ("example@-invalid.com", false), + ("example@invalid.com-", false), + ("example@inv-.alid-.com", false), + ("example@inv-.-alid.com", false), + (r#"test@example.com\n\n