aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.editorconfig5
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml11
-rw-r--r--LICENSE22
-rw-r--r--README.md118
-rw-r--r--validator/Cargo.toml15
-rw-r--r--validator/src/email.rs121
-rw-r--r--validator/src/ip.rs107
-rw-r--r--validator/src/length.rs94
-rw-r--r--validator/src/lib.rs20
-rw-r--r--validator/src/range.rs30
-rw-r--r--validator/src/types.rs30
-rw-r--r--validator/src/urls.rs30
-rw-r--r--validator_derive/Cargo.toml25
-rw-r--r--validator_derive/src/lib.rs410
-rw-r--r--validator_derive/tests/compile-fail/custom_not_string.rs15
-rw-r--r--validator_derive/tests/compile-fail/length/equal_and_min_max_set.rs15
-rw-r--r--validator_derive/tests/compile-fail/length/no_args.rs15
-rw-r--r--validator_derive/tests/compile-fail/length/unknown_arg.rs15
-rw-r--r--validator_derive/tests/compile-fail/length/wrong_arg_type.rs15
-rw-r--r--validator_derive/tests/compile-fail/length/wrong_type.rs15
-rw-r--r--validator_derive/tests/compile-fail/no_validations.rs15
-rw-r--r--validator_derive/tests/compile-fail/range/missing_arg.rs15
-rw-r--r--validator_derive/tests/compile-fail/range/no_args.rs15
-rw-r--r--validator_derive/tests/compile-fail/range/unknown_arg.rs15
-rw-r--r--validator_derive/tests/compile-fail/range/wrong_arg_type.rs15
-rw-r--r--validator_derive/tests/compile-fail/range/wrong_type.rs15
-rw-r--r--validator_derive/tests/compile_test.rs24
-rw-r--r--validator_derive/tests/run-pass/custom.rs17
-rw-r--r--validator_derive/tests/run-pass/email.rs13
-rw-r--r--validator_derive/tests/run-pass/length.rs28
-rw-r--r--validator_derive/tests/run-pass/range.rs27
-rw-r--r--validator_derive/tests/run-pass/url.rs13
-rw-r--r--validator_derive/tests/test_derive.rs136
34 files changed, 1480 insertions, 0 deletions
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<String> {
+ if username == "xXxShad0wxXx" {
+ return Some("terrible_username".to_string());
+ }
+
+ None
+}
+
+// load the struct from some json...
+// `validate` returns `Result<(), HashMap<String, Vec<String>>>`
+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<String>` 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 <vincent@wearewizards.io>"]
+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<script src="x.js">"#, false),
+ (r#""\\\011"@here.com"#, false),
+ (r#""\\\012"@here.com"#, false),
+ ("trailingdot@shouldfail.com.", false),
+ // Trailing newlines in username or domain not allowed
+ ("a@b.com\n", false),
+ ("a\n@b.com", false),
+ (r#""test@test"\n@example.com"#, false),
+ ("a@[127.0.0.1]\n", false),
+ ];
+
+ for (input, expected) in tests {
+ println!("{} - {}", input, expected);
+ assert_eq!(validate_email(input), expected);
+ }
+ }
+}
diff --git a/validator/src/ip.rs b/validator/src/ip.rs
new file mode 100644
index 0000000..9a94522
--- /dev/null
+++ b/validator/src/ip.rs
@@ -0,0 +1,107 @@
+use std::str::FromStr;
+use std::net::IpAddr;
+
+
+/// Validates whether the given string is an IP V4
+pub fn validate_ip_v4(val: &str) -> bool {
+ match IpAddr::from_str(val) {
+ Ok(i) => match i {
+ IpAddr::V4(_) => true,
+ IpAddr::V6(_) => false,
+ },
+ Err(_) => false,
+ }
+}
+
+/// Validates whether the given string is an IP V6
+pub fn validate_ip_v6(val: &str) -> bool {
+ match IpAddr::from_str(val) {
+ Ok(i) => match i {
+ IpAddr::V4(_) => false,
+ IpAddr::V6(_) => true,
+ },
+ Err(_) => false,
+ }
+}
+
+/// Validates whether the given string is an IP
+pub fn validate_ip(val: &str) -> bool {
+ match IpAddr::from_str(val) {
+ Ok(_) => true,
+ Err(_) => false,
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::{validate_ip_v4, validate_ip_v6, validate_ip};
+
+ #[test]
+ fn test_validate_ip() {
+ let tests = vec![
+ ("1.1.1.1", true),
+ ("255.0.0.0", true),
+ ("0.0.0.0", true),
+ ("256.1.1.1", false),
+ ("25.1.1.", false),
+ ("25,1,1,1", false),
+ ("fe80::223:6cff:fe8a:2e8a", true),
+ ("::ffff:254.42.16.14", true),
+ ("2a02::223:6cff :fe8a:2e8a", false),
+ ];
+
+ for (input, expected) in tests {
+ assert_eq!(validate_ip(input), expected);
+ }
+ }
+
+ #[test]
+ fn test_validate_ip_v4() {
+ let tests = vec![
+ ("1.1.1.1", true),
+ ("255.0.0.0", true),
+ ("0.0.0.0", true),
+ ("256.1.1.1", false),
+ ("25.1.1.", false),
+ ("25,1,1,1", false),
+ ("25.1 .1.1", false),
+ ("1.1.1.1\n", false),
+ ("٧.2٥.3٣.243", false),
+ ];
+
+ for (input, expected) in tests {
+ assert_eq!(validate_ip_v4(input), expected);
+ }
+ }
+
+ #[test]
+ fn test_validate_ip_v6() {
+ let tests = vec![
+ ("fe80::223:6cff:fe8a:2e8a", true),
+ ("2a02::223:6cff:fe8a:2e8a", true),
+ ("1::2:3:4:5:6:7", true),
+ ("::", true),
+ ("::a", true),
+ ("2::", true),
+ ("::ffff:254.42.16.14", true),
+ ("::ffff:0a0a:0a0a", true),
+ ("::254.42.16.14", true),
+ ("::0a0a:0a0a", true),
+ ("foo", false),
+ ("127.0.0.1", false),
+ ("12345::", false),
+ ("1::2::3::4", false),
+ ("1::zzz", false),
+ ("1:2", false),
+ ("fe80::223: 6cff:fe8a:2e8a", false),
+ ("2a02::223:6cff :fe8a:2e8a", false),
+ ("::ffff:999.42.16.14", false),
+ ("::ffff:zzzz:0a0a", false),
+ ];
+
+ for (input, expected) in tests {
+ assert_eq!(validate_ip_v6(input), expected);
+ }
+ }
+}
diff --git a/validator/src/length.rs b/validator/src/length.rs
new file mode 100644
index 0000000..358317f
--- /dev/null
+++ b/validator/src/length.rs
@@ -0,0 +1,94 @@
+use types::Validator;
+
+// a bit sad but we can generically refer to a struct that has a len() method
+// so we impl our own trait for it
+pub trait HasLen {
+ fn length(&self) -> u64;
+}
+
+impl<'a> HasLen for &'a String {
+ fn length(&self) -> u64 {
+ self.len() as u64
+ }
+}
+
+impl<'a> HasLen for &'a str {
+ fn length(&self) -> u64 {
+ self.len() as u64
+ }
+}
+
+impl<T> HasLen for Vec<T> {
+ fn length(&self) -> u64 {
+ self.len() as u64
+ }
+}
+impl<'a, T> HasLen for &'a Vec<T> {
+ fn length(&self) -> u64 {
+ self.len() as u64
+ }
+}
+
+/// Validates the length of the value given.
+/// If the validator has `equal` set, it will ignore any `min` and `max` value.
+///
+/// If you apply it on String, don't forget that the length can be different
+/// from the number of visual characters for Unicode
+pub fn validate_length<T: HasLen>(length: Validator, val: T) -> bool {
+ match length {
+ Validator::Length { min, max, equal } => {
+ let val_length = val.length();
+ if let Some(eq) = equal {
+ return val_length == eq;
+ }
+ if let Some(m) = min {
+ if val_length < m {
+ return false;
+ }
+ }
+ if let Some(m) = max {
+ if val_length > m {
+ return false;
+ }
+ }
+ },
+ _ => unreachable!()
+ }
+
+ true
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{validate_length, Validator};
+
+ #[test]
+ fn test_validate_length_equal_overrides_min_max() {
+ let validator = Validator::Length { min: Some(1), max: Some(2), equal: Some(5) };
+ assert_eq!(validate_length(validator, "hello"), true);
+ }
+
+ #[test]
+ fn test_validate_length_string_min_max() {
+ let validator = Validator::Length { min: Some(1), max: Some(10), equal: None };
+ assert_eq!(validate_length(validator, "hello"), true);
+ }
+
+ #[test]
+ fn test_validate_length_string_min_only() {
+ let validator = Validator::Length { min: Some(10), max: None, equal: None };
+ assert_eq!(validate_length(validator, "hello"), false);
+ }
+
+ #[test]
+ fn test_validate_length_string_max_only() {
+ let validator = Validator::Length { min: None, max: Some(1), equal: None };
+ assert_eq!(validate_length(validator, "hello"), false);
+ }
+
+ #[test]
+ fn test_validate_length_vec() {
+ let validator = Validator::Length { min: None, max: None, equal: Some(3) };
+ assert_eq!(validate_length(validator, vec![1, 2, 3]), true);
+ }
+}
diff --git a/validator/src/lib.rs b/validator/src/lib.rs
new file mode 100644
index 0000000..181fc54
--- /dev/null
+++ b/validator/src/lib.rs
@@ -0,0 +1,20 @@
+extern crate url;
+extern crate regex;
+#[macro_use] extern crate lazy_static;
+extern crate idna;
+
+
+mod types;
+mod ip;
+mod email;
+mod length;
+mod range;
+mod urls;
+
+
+pub use types::{Errors, Validate, Validator};
+pub use ip::{validate_ip, validate_ip_v4, validate_ip_v6};
+pub use email::{validate_email};
+pub use length::{HasLen, validate_length};
+pub use range::{validate_range};
+pub use urls::{validate_url};
diff --git a/validator/src/range.rs b/validator/src/range.rs
new file mode 100644
index 0000000..8db9a86
--- /dev/null
+++ b/validator/src/range.rs
@@ -0,0 +1,30 @@
+use types::Validator;
+
+/// Validates that a number is in the given range
+///
+/// TODO: see if can be generic over the number type
+pub fn validate_range(range: Validator, val: f64) -> bool {
+ match range {
+ Validator::Range { min, max } => {
+ val >= min && val <= max
+ },
+ _ => unreachable!()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{validate_range, Validator};
+
+ #[test]
+ fn test_validate_range_ok() {
+ let validator = Validator::Range { min: 0.0, max: 10.0 };
+ assert_eq!(validate_range(validator, 1 as f64), true);
+ }
+
+ #[test]
+ fn test_validate_range_fail() {
+ let validator = Validator::Range { min: 0.0, max: 10.0 };
+ assert_eq!(validate_range(validator, 20 as f64), false);
+ }
+}
diff --git a/validator/src/types.rs b/validator/src/types.rs
new file mode 100644
index 0000000..8265c63
--- /dev/null
+++ b/validator/src/types.rs
@@ -0,0 +1,30 @@
+use std::collections::HashMap;
+
+
+pub type Errors = HashMap<String, Vec<String>>;
+
+pub trait Validate {
+ //fn load_and_validate<T>(data: &str) -> Result<T, Errors>;
+ fn validate(&self) -> Result<(), Errors>;
+}
+
+#[derive(Debug, Clone)]
+pub enum Validator {
+ // String is the path to the function
+ Custom(String),
+ // value is a &str
+ Email,
+ // value is a &str
+ Url,
+ // value is a number
+ Range {
+ min: f64,
+ max: f64,
+ },
+ // value is anything that impl HasLen
+ Length {
+ min: Option<u64>,
+ max: Option<u64>,
+ equal: Option<u64>,
+ },
+}
diff --git a/validator/src/urls.rs b/validator/src/urls.rs
new file mode 100644
index 0000000..d948050
--- /dev/null
+++ b/validator/src/urls.rs
@@ -0,0 +1,30 @@
+use url::Url;
+
+
+/// Validates whether the string given is a url
+pub fn validate_url(val: &str) -> bool {
+ match Url::parse(val) {
+ Ok(_) => true,
+ Err(_) => false,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{validate_url};
+
+
+ #[test]
+ fn test_validate_url() {
+ let tests = vec![
+ ("http", false),
+ ("https://google.com", true),
+ ("http://localhost:80", true),
+ ("ftp://localhost:80", true),
+ ];
+
+ for (url, expected) in tests {
+ assert_eq!(validate_url(url), expected);
+ }
+ }
+}
diff --git a/validator_derive/Cargo.toml b/validator_derive/Cargo.toml
new file mode 100644
index 0000000..76c5e7e
--- /dev/null
+++ b/validator_derive/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "validator_derive"
+version = "0.1.0"
+authors = ["Vincent Prouillet <vincent@wearewizards.io>"]
+license = "MIT"
+description = "Macros 1.1 implementation of #[derive(Validate)]"
+homepage = "https://github.com/Keats/validator"
+repository = "https://github.com/Keats/validator"
+keywords = ["validation", "api", "validator"]
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = "0.10"
+quote = "0.3"
+
+[dev-dependencies]
+serde = "0.8"
+serde_derive = "0.8"
+serde_json = "0.8"
+compiletest_rs = "*"
+
+[dependencies.validator]
+path = "../validator"
diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs
new file mode 100644
index 0000000..0e580b0
--- /dev/null
+++ b/validator_derive/src/lib.rs
@@ -0,0 +1,410 @@
+#![feature(proc_macro, proc_macro_lib)]
+#![recursion_limit = "128"]
+
+#[macro_use] extern crate quote;
+extern crate proc_macro;
+extern crate syn;
+extern crate validator;
+
+
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use validator::{Validator};
+
+
+static LENGTH_TYPES: [&'static str; 2] = ["String", "Vec"];
+static RANGE_TYPES: [&'static str; 12] = [
+ "usize", "u8", "u16", "u32", "u64", "isize", "i8", "i16", "i32", "i64", "f32", "f64"
+];
+
+
+#[proc_macro_derive(Validate, attributes(validate))]
+pub fn derive_validation(input: TokenStream) -> TokenStream {
+ let source = input.to_string();
+ // Parse the string representation to an AST
+ let ast = syn::parse_macro_input(&source).unwrap();
+
+ let expanded = expand_validation(&ast);
+ expanded.parse().unwrap()
+}
+
+
+fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens {
+ let fields = match ast.body {
+ syn::Body::Struct(syn::VariantData::Struct(ref fields)) => {
+ if fields.iter().any(|field| field.ident.is_none()) {
+ panic!("struct has unnamed fields");
+ }
+ fields
+ },
+ _ => panic!("#[derive(Validate)] can only be used with structs"),
+ };
+
+ let mut validations = vec![];
+
+ for field in fields {
+ let field_ident = match field.ident {
+ Some(ref i) => i,
+ None => unreachable!()
+ };
+
+ let (name, validators) = find_validators_for_field(field);
+ for validator in &validators {
+ validations.push(match validator {
+ &Validator::Length {min, max, equal} => {
+ // Can't interpolate None
+ let min_tokens = option_u64_to_tokens(min);
+ let max_tokens = option_u64_to_tokens(max);
+ let equal_tokens = option_u64_to_tokens(equal);
+ quote!(
+ if !::validator::validate_length(
+ ::validator::Validator::Length {
+ min: #min_tokens,
+ max: #max_tokens,
+ equal: #equal_tokens
+ },
+ &self.#field_ident
+ ) {
+ errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("length".to_string());
+ }
+ )
+ },
+ &Validator::Range {min, max} => {
+ quote!(
+ if !::validator::validate_range(
+ ::validator::Validator::Range {min: #min, max: #max},
+ self.#field_ident as f64
+ ) {
+ errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("range".to_string());
+ }
+ )
+ },
+ &Validator::Email => {
+ quote!(
+ if !::validator::validate_email(&self.#field_ident) {
+ errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("email".to_string());
+ }
+ )
+ }
+ &Validator::Url => {
+ quote!(
+ if !::validator::validate_url(&self.#field_ident) {
+ errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("url".to_string());
+ }
+ )
+ },
+ &Validator::Custom(ref f) => {
+ let fn_ident = syn::Ident::new(f.clone());
+ quote!(
+ match #fn_ident(&self.#field_ident) {
+ ::std::option::Option::Some(s) => errors.entry(#name.to_string()).or_insert_with(|| vec![]).push(s),
+ ::std::option::Option::None => (),
+ };
+ )
+ },
+ });
+ }
+ }
+
+ let ident = &ast.ident;
+ let impl_ast = quote!(
+ impl Validate for #ident {
+ fn validate(&self) -> ::std::result::Result<(), ::validator::Errors> {
+ use std::collections::HashMap;
+ let mut errors = HashMap::new();
+
+ #(#validations)*
+
+ if errors.is_empty() {
+ ::std::result::Result::Ok(())
+ } else {
+ ::std::result::Result::Err(errors)
+ }
+ }
+ }
+ );
+ // println!("{}", impl_ast.to_string());
+ impl_ast
+}
+
+/// Find everything we need to know about a Field.
+fn find_validators_for_field(field: &syn::Field) -> (String, Vec<Validator>) {
+ let mut field_name = match field.ident {
+ Some(ref s) => s.to_string(),
+ None => unreachable!(),
+ };
+
+ let error = |msg: &str| -> ! {
+ panic!("Invalid attribute #[validate] on field `{}`: {}", field.ident.clone().unwrap().to_string(), msg);
+ };
+
+ let field_type = match field.ty {
+ syn::Ty::Path(_, ref p) => {
+ p.segments[0].ident.to_string()
+
+ },
+ _ => error(&format!("Type `{:?}` not supported", field.ty))
+ };
+
+ let mut validators = vec![];
+
+ let find_struct_validator = |name: String, meta_items: &Vec<syn::NestedMetaItem>| -> Validator {
+ match name.as_ref() {
+ "length" => {
+ let mut min = None;
+ let mut max = None;
+ let mut equal = None;
+
+ for meta_item in meta_items {
+ match *meta_item {
+ syn::NestedMetaItem::MetaItem(ref item) => match *item {
+ syn::MetaItem::NameValue(ref name, ref val) => {
+ match name.to_string().as_ref() {
+ "min" => {
+ min = match lit_to_int(val) {
+ Some(s) => Some(s),
+ None => error("invalid argument type for `min` of `length` validator: only integers are allowed"),
+ };
+ },
+ "max" => {
+ max = match lit_to_int(val) {
+ Some(s) => Some(s),
+ None => error("invalid argument type for `max` of `length` validator: only integers are allowed"),
+ };
+ },
+ "equal" => {
+ equal = match lit_to_int(val) {
+ Some(s) => Some(s),
+ None => error("invalid argument type for `equal` of `length` validator: only integers are allowed"),
+ };
+ },
+ _ => error(&format!(
+ "unknown argument `{}` for validator `length` (it only has `min`, `max`, `equal`)",
+ name.to_string()
+ ))
+ }
+ },
+ _ => panic!("unexpected item {:?} while parsing `length` validator", item)
+ },
+ _=> unreachable!()
+ }
+ }
+ if equal.is_some() && (min.is_some() || max.is_some()) {
+ error("both `equal` and `min` or `max` have been set in `length` validator: probably a mistake");
+ }
+ Validator::Length { min: min, max: max, equal: equal }
+ },
+ "range" => {
+ let mut min = 0.0;
+ let mut max = 0.0;
+ for meta_item in meta_items {
+ match *meta_item {
+ syn::NestedMetaItem::MetaItem(ref item) => match *item {
+ syn::MetaItem::NameValue(ref name, ref val) => {
+ match name.to_string().as_ref() {
+ "min" => {
+ min = match lit_to_float(val) {
+ Some(s) => s,
+ None => error("invalid argument type for `min` of `range` validator: only integers are allowed")
+ };
+ },
+ "max" => {
+ max = match lit_to_float(val) {
+ Some(s) => s,
+ None => error("invalid argument type for `max` of `range` validator: only integers are allowed")
+ };
+ },
+ _ => error(&format!(
+ "unknown argument `{}` for validator `range` (it only has `min`, `max`)",
+ name.to_string()
+ ))
+ }
+ },
+ _ => panic!("unexpected item {:?} while parsing `range` validator", item)
+ },
+ _=> unreachable!()
+ }
+ }
+
+ Validator::Range { min: min, max: max}
+ }
+ _ => panic!("unexpected list validator: {:?}", name)
+ }
+ };
+
+ for attr in &field.attrs {
+ if attr.name() != "validate" && attr.name() != "serde" {
+ continue;
+ }
+
+ match attr.value {
+ syn::MetaItem::List(_, ref meta_items) => {
+ if attr.name() == "serde" {
+ match find_original_field_name(meta_items) {
+ Some(s) => { field_name = s },
+ None => ()
+ };
+ continue;
+ }
+
+ // only validation from there on
+ for meta_item in meta_items {
+ match *meta_item {
+ syn::NestedMetaItem::MetaItem(ref item) => match *item {
+ // email, url
+ syn::MetaItem::Word(ref name) => match name.to_string().as_ref() {
+ "email" => {
+ if field_type != "String" {
+ panic!("`email` validator can only be used on String");
+ }
+ validators.push(Validator::Email);
+ },
+ "url" => {
+ if field_type != "String" {
+ panic!("`url` validator can only be used on String");
+ }
+ validators.push(Validator::Url);
+ },
+ _ => panic!("Unexpected word validator: {}", name)
+ },
+ // custom
+ syn::MetaItem::NameValue(ref name, ref val) => {
+ if name == "custom" {
+ match lit_to_string(val) {
+ Some(s) => validators.push(Validator::Custom(s)),
+ None => error("invalid argument for `custom` validator: only strings are allowed"),
+ };
+ } else {
+ panic!("unexpected name value validator: {:?}", name);
+ }
+ },
+ // validators with args: length for example
+ syn::MetaItem::List(ref name, ref meta_items) => {
+ // Some sanity checking first
+ if name == "length" {
+ if !LENGTH_TYPES.contains(&field_type.as_ref()) {
+ error(&format!(
+ "Validator `length` can only be used on types `String` or `Vec` but found `{}`",
+ field_type
+ ));
+ }
+
+ if meta_items.len() == 0 {
+ error("Validator `length` requires at least 1 argument out of `min`, `max` and `equal`");
+ }
+ }
+
+ if name == "range" {
+ if !RANGE_TYPES.contains(&field_type.as_ref()) {
+ error(&format!(
+ "Validator `range` can only be used on number types but found `{}`",
+ field_type
+ ));
+ }
+
+ if meta_items.len() != 2 {
+ error("Validator `range` requires 2 arguments: `min` and `max`");
+ }
+ }
+
+ validators.push(find_struct_validator(name.to_string(), meta_items));
+ },
+ },
+ _ => unreachable!("Found a non MetaItem while looking for validators")
+ };
+ }
+ },
+ _ => unreachable!("Got something other than a list of attributes while checking field `{}`", field_name),
+ }
+ }
+
+ if validators.is_empty() {
+ error("it needs at least one validator");
+ }
+
+ (field_name, validators)
+}
+
+/// Serde can be used to rename fields on deserialization but most of the times
+/// we want the error on the original field.
+///
+/// For example a JS frontend might send camelCase fields and Rust converts them to snake_case
+/// but we want to send the errors back to the frontend with the original name
+fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<String> {
+ let mut original_name = None;
+
+ for meta_item in meta_items {
+ match *meta_item {
+ syn::NestedMetaItem::MetaItem(ref item) => match *item {
+ syn::MetaItem::Word(_) => continue,
+ syn::MetaItem::NameValue(ref name, ref val) => {
+ if name == "rename" {
+ original_name = Some(lit_to_string(val).unwrap());
+ }
+
+ },
+ // length
+ syn::MetaItem::List(_, ref meta_items) => {
+ return find_original_field_name(meta_items);
+ }
+ },
+ _ => unreachable!()
+ };
+
+ if original_name.is_some() {
+ return original_name;
+ }
+ }
+
+ original_name
+}
+
+//
+//fn quote_length_validator(min: Option<u64>, max: Option<u64>, equal: Option<u64>) {
+//
+//}
+
+fn lit_to_string(lit: &syn::Lit) -> Option<String> {
+ match *lit {
+ syn::Lit::Str(ref s, _) => Some(s.to_string()),
+ _ => None,
+ }
+}
+
+fn lit_to_int(lit: &syn::Lit) -> Option<u64> {
+ match *lit {
+ syn::Lit::Int(ref s, _) => Some(*s),
+ _ => None,
+ }
+}
+
+fn lit_to_float(lit: &syn::Lit) -> Option<f64> {
+ match *lit {
+ syn::Lit::Float(ref s, _) => Some(s.parse::<f64>().unwrap()),
+ syn::Lit::Int(ref s, _) => Some(*s as f64),
+ _ => None,
+ }
+}
+
+fn option_u64_to_tokens(opt: Option<u64>) -> quote::Tokens {
+ let mut tokens = quote::Tokens::new();
+ tokens.append("::");
+ tokens.append("std");
+ tokens.append("::");
+ tokens.append("option");
+ tokens.append("::");
+ tokens.append("Option");
+ tokens.append("::");
+ match opt {
+ Some(ref t) => {
+ tokens.append("Some");
+ tokens.append("(");
+ t.to_tokens(&mut tokens);
+ tokens.append(")");
+ }
+ None => {
+ tokens.append("None");
+ }
+ }
+ tokens
+}
diff --git a/validator_derive/tests/compile-fail/custom_not_string.rs b/validator_derive/tests/compile-fail/custom_not_string.rs
new file mode 100644
index 0000000..e576c5c
--- /dev/null
+++ b/validator_derive/tests/compile-fail/custom_not_string.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: invalid argument for `custom` validator: only strings are allowed
+struct Test {
+ #[validate(custom = 2)]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/length/equal_and_min_max_set.rs b/validator_derive/tests/compile-fail/length/equal_and_min_max_set.rs
new file mode 100644
index 0000000..cc6654c
--- /dev/null
+++ b/validator_derive/tests/compile-fail/length/equal_and_min_max_set.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: both `equal` and `min` or `max` have been set in `length` validator: probably a mistake
+struct Test {
+ #[validate(length(min = 1, equal = 2))]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/length/no_args.rs b/validator_derive/tests/compile-fail/length/no_args.rs
new file mode 100644
index 0000000..35a44df
--- /dev/null
+++ b/validator_derive/tests/compile-fail/length/no_args.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: Validator `length` requires at least 1 argument out of `min`, `max` and `equal`
+struct Test {
+ #[validate(length())]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/length/unknown_arg.rs b/validator_derive/tests/compile-fail/length/unknown_arg.rs
new file mode 100644
index 0000000..d8e6185
--- /dev/null
+++ b/validator_derive/tests/compile-fail/length/unknown_arg.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: unknown argument `eq` for validator `length` (it only has `min`, `max`, `equal`)
+struct Test {
+ #[validate(length(eq = 2))]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/length/wrong_arg_type.rs b/validator_derive/tests/compile-fail/length/wrong_arg_type.rs
new file mode 100644
index 0000000..4d3f602
--- /dev/null
+++ b/validator_derive/tests/compile-fail/length/wrong_arg_type.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: invalid argument type for `min` of `length` validator: only integers are allowed
+struct Test {
+ #[validate(length(min = "2"))]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/length/wrong_type.rs b/validator_derive/tests/compile-fail/length/wrong_type.rs
new file mode 100644
index 0000000..e8a28ca
--- /dev/null
+++ b/validator_derive/tests/compile-fail/length/wrong_type.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: Validator `length` can only be used on types `String` or `Vec` but found `usize`
+struct Test {
+ #[validate(length())]
+ s: usize,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/no_validations.rs b/validator_derive/tests/compile-fail/no_validations.rs
new file mode 100644
index 0000000..0a65bdd
--- /dev/null
+++ b/validator_derive/tests/compile-fail/no_validations.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: it needs at least one validator
+struct Test {
+ #[validate()]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/range/missing_arg.rs b/validator_derive/tests/compile-fail/range/missing_arg.rs
new file mode 100644
index 0000000..94913cf
--- /dev/null
+++ b/validator_derive/tests/compile-fail/range/missing_arg.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: Validator `range` requires 2 arguments: `min` and `max`
+struct Test {
+ #[validate(range(min = 2.0))]
+ s: i32,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/range/no_args.rs b/validator_derive/tests/compile-fail/range/no_args.rs
new file mode 100644
index 0000000..0532cc0
--- /dev/null
+++ b/validator_derive/tests/compile-fail/range/no_args.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: Validator `range` requires 2 arguments: `min` and `max`
+struct Test {
+ #[validate(range())]
+ s: i32,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/range/unknown_arg.rs b/validator_derive/tests/compile-fail/range/unknown_arg.rs
new file mode 100644
index 0000000..7d2ac57
--- /dev/null
+++ b/validator_derive/tests/compile-fail/range/unknown_arg.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: unknown argument `mi` for validator `range` (it only has `min`, `max`)
+struct Test {
+ #[validate(range(mi = 2, max = 3))]
+ s: i32,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/range/wrong_arg_type.rs b/validator_derive/tests/compile-fail/range/wrong_arg_type.rs
new file mode 100644
index 0000000..a62a5ff
--- /dev/null
+++ b/validator_derive/tests/compile-fail/range/wrong_arg_type.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: invalid argument type for `min` of `range` validator: only integers are allowed
+struct Test {
+ #[validate(range(min = "2", max = 3))]
+ s: i32,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile-fail/range/wrong_type.rs b/validator_derive/tests/compile-fail/range/wrong_type.rs
new file mode 100644
index 0000000..e341364
--- /dev/null
+++ b/validator_derive/tests/compile-fail/range/wrong_type.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `s`: Validator `range` can only be used on number types but found `String`
+struct Test {
+ #[validate(range(min = 10.0, max = 12.0))]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/compile_test.rs b/validator_derive/tests/compile_test.rs
new file mode 100644
index 0000000..b6fe8e5
--- /dev/null
+++ b/validator_derive/tests/compile_test.rs
@@ -0,0 +1,24 @@
+extern crate compiletest_rs as compiletest;
+
+use std::path::PathBuf;
+
+fn run_mode(mode: &'static str) {
+ let mut config = compiletest::default_config();
+ let cfg_mode = mode.parse().expect("Invalid mode");
+
+ config.target_rustcflags = Some("-L target/debug/ -L target/debug/deps/".to_string());
+ config.mode = cfg_mode;
+ config.src_base = PathBuf::from(format!("tests/{}", mode));
+
+ compiletest::run_tests(&config);
+}
+
+#[test]
+fn test_compile_fail() {
+ run_mode("compile-fail");
+}
+
+#[test]
+fn test_run_pass() {
+ run_mode("run-pass");
+}
diff --git a/validator_derive/tests/run-pass/custom.rs b/validator_derive/tests/run-pass/custom.rs
new file mode 100644
index 0000000..205198e
--- /dev/null
+++ b/validator_derive/tests/run-pass/custom.rs
@@ -0,0 +1,17 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+struct Test {
+ #[validate(custom = "validate_something")]
+ s: String,
+}
+
+fn validate_something(s: &str) -> Option<String> {
+ Some(s.to_string())
+}
+
+fn main() {}
diff --git a/validator_derive/tests/run-pass/email.rs b/validator_derive/tests/run-pass/email.rs
new file mode 100644
index 0000000..edfc357
--- /dev/null
+++ b/validator_derive/tests/run-pass/email.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(email)]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/run-pass/length.rs b/validator_derive/tests/run-pass/length.rs
new file mode 100644
index 0000000..01b85ea
--- /dev/null
+++ b/validator_derive/tests/run-pass/length.rs
@@ -0,0 +1,28 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+struct Test {
+ #[validate(length(min = 1))]
+ s: String,
+ #[validate(length(min = 1, max = 2))]
+ s2: String,
+ #[validate(length(equal = 1))]
+ s3: String,
+ #[validate(length(max = 1))]
+ s4: String,
+
+ #[validate(length(min = 1))]
+ s5: Vec<String>,
+ #[validate(length(min = 1, max = 2))]
+ s6: Vec<String>,
+ #[validate(length(equal = 1))]
+ s7: Vec<String>,
+ #[validate(length(max = 1))]
+ s8: Vec<String>,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/run-pass/range.rs b/validator_derive/tests/run-pass/range.rs
new file mode 100644
index 0000000..8f3a047
--- /dev/null
+++ b/validator_derive/tests/run-pass/range.rs
@@ -0,0 +1,27 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+struct Test {
+ #[validate(range(min = 1, max = 2.2))]
+ s: isize,
+ #[validate(range(min = 1, max = 2))]
+ s2: usize,
+ #[validate(range(min = 18, max = 22))]
+ s3: i32,
+ #[validate(range(min = 18, max = 22))]
+ s4: i64,
+ #[validate(range(min = 18, max = 22))]
+ s5: u32,
+ #[validate(range(min = 18, max = 22))]
+ s6: u64,
+ #[validate(range(min = 18.1, max = 22))]
+ s7: i8,
+ #[validate(range(min = 18.0, max = 22))]
+ s8: u8,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/run-pass/url.rs b/validator_derive/tests/run-pass/url.rs
new file mode 100644
index 0000000..910676a
--- /dev/null
+++ b/validator_derive/tests/run-pass/url.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(url)]
+ s: String,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/test_derive.rs b/validator_derive/tests/test_derive.rs
new file mode 100644
index 0000000..1e310b1
--- /dev/null
+++ b/validator_derive/tests/test_derive.rs
@@ -0,0 +1,136 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+#[macro_use] extern crate serde_derive;
+extern crate serde_json;
+
+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<String> {
+ if username == "xXxShad0wxXx" {
+ return Some("terrible_username".to_string());
+ }
+
+ None
+}
+
+#[test]
+fn test_can_validate_ok() {
+ let signup = SignupData {
+ mail: "bob@bob.com".to_string(),
+ site: "http://hello.com".to_string(),
+ first_name: "Bob".to_string(),
+ age: 18,
+ };
+
+ assert!(signup.validate().is_ok());
+}
+
+#[test]
+fn test_bad_email_fails_validation() {
+ let signup = SignupData {
+ mail: "bob".to_string(),
+ site: "http://hello.com".to_string(),
+ first_name: "Bob".to_string(),
+ age: 18,
+ };
+ let res = signup.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err();
+ assert!(errs.contains_key("mail"));
+ assert_eq!(errs["mail"], vec!["email".to_string()]);
+}
+
+#[test]
+fn test_bad_url_fails_validation() {
+ let signup = SignupData {
+ mail: "bob@bob.com".to_string(),
+ site: "//hello.com".to_string(),
+ first_name: "Bob".to_string(),
+ age: 18,
+ };
+ let res = signup.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err();
+ assert!(errs.contains_key("site"));
+ assert_eq!(errs["site"], vec!["url".to_string()]);
+}
+
+#[test]
+fn test_bad_length_fails_validation_and_points_to_original_name() {
+ let signup = SignupData {
+ mail: "bob@bob.com".to_string(),
+ site: "http://hello.com".to_string(),
+ first_name: "".to_string(),
+ age: 18,
+ };
+ let res = signup.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err();
+ println!("{:?}", errs);
+ assert!(errs.contains_key("firstName"));
+ assert_eq!(errs["firstName"], vec!["length".to_string()]);
+}
+
+
+#[test]
+fn test_bad_range_fails_validation() {
+ let signup = SignupData {
+ mail: "bob@bob.com".to_string(),
+ site: "https://hello.com".to_string(),
+ first_name: "Bob".to_string(),
+ age: 1,
+ };
+ let res = signup.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err();
+ assert!(errs.contains_key("age"));
+ assert_eq!(errs["age"], vec!["range".to_string()]);
+}
+
+#[test]
+fn test_can_have_multiple_errors() {
+ let signup = SignupData {
+ mail: "bob@bob.com".to_string(),
+ site: "https://hello.com".to_string(),
+ first_name: "".to_string(),
+ age: 1,
+ };
+ let res = signup.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err();
+ assert!(errs.contains_key("age"));
+ assert!(errs.contains_key("firstName"));
+ assert_eq!(errs["age"], vec!["range".to_string()]);
+ assert_eq!(errs["firstName"], vec!["length".to_string()]);
+}
+
+#[test]
+fn test_custom_validation_error() {
+ let signup = SignupData {
+ mail: "bob@bob.com".to_string(),
+ site: "https://hello.com".to_string(),
+ first_name: "xXxShad0wxXx".to_string(),
+ age: 18,
+ };
+ let res = signup.validate();
+ assert!(res.is_err());
+ let errs = res.unwrap_err();
+ assert!(errs.contains_key("firstName"));
+ assert_eq!(errs["firstName"], vec!["terrible_username".to_string()]);
+}