├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml ├── validator ├── Cargo.toml ├── src │ ├── display_impl.rs │ ├── lib.rs │ ├── traits.rs │ ├── types.rs │ └── validation │ │ ├── cards.rs │ │ ├── contains.rs │ │ ├── does_not_contain.rs │ │ ├── email.rs │ │ ├── ip.rs │ │ ├── length.rs │ │ ├── mod.rs │ │ ├── must_match.rs │ │ ├── non_control_character.rs │ │ ├── range.rs │ │ ├── regex.rs │ │ ├── required.rs │ │ └── urls.rs └── tests │ └── display.rs ├── validator_derive ├── Cargo.toml └── src │ ├── lib.rs │ ├── tokens │ ├── cards.rs │ ├── contains.rs │ ├── custom.rs │ ├── does_not_contain.rs │ ├── email.rs │ ├── ip.rs │ ├── length.rs │ ├── mod.rs │ ├── must_match.rs │ ├── nested.rs │ ├── non_control_character.rs │ ├── range.rs │ ├── regex.rs │ ├── required.rs │ ├── schema.rs │ └── url.rs │ ├── types.rs │ └── utils.rs └── validator_derive_tests ├── Cargo.toml └── tests ├── compile-fail ├── custom │ ├── custom_not_string.rs │ ├── custom_not_string.stderr │ ├── defined_args_in_custom.rs │ ├── defined_args_in_custom.stderr │ ├── different_lifetime.rs │ ├── different_lifetime.stderr │ ├── missing_function_arg.rs │ └── missing_function_arg.stderr ├── length │ ├── equal_and_min_max_set.rs │ ├── equal_and_min_max_set.stderr │ ├── no_args.rs │ ├── no_args.stderr │ ├── unknown_arg.rs │ ├── unknown_arg.stderr │ ├── wrong_type.rs │ └── wrong_type.stderr ├── must_match │ ├── field_doesnt_exist.rs │ ├── field_doesnt_exist.stderr │ ├── field_type_doesnt_match.rs │ ├── field_type_doesnt_match.stderr │ ├── unexpected_name_value.rs │ └── unexpected_name_value.stderr ├── no_nested_validations.rs ├── no_nested_validations.stderr ├── not_a_struct.rs ├── not_a_struct.stderr ├── range │ ├── no_args.rs │ ├── no_args.stderr │ ├── unknown_arg.rs │ └── unknown_arg.stderr ├── schema │ ├── missing_function.rs │ └── missing_function.stderr ├── unexpected_list_validator.rs ├── unexpected_list_validator.stderr ├── unexpected_validator.rs ├── unexpected_validator.stderr ├── unnamed_fields.rs ├── unnamed_fields.stderr ├── wrong_crate_alias.rs └── wrong_crate_alias.stderr ├── compile_test.rs ├── complex.rs ├── contains.rs ├── credit_card.rs ├── custom.rs ├── custom_args.rs ├── does_not_contain.rs ├── email.rs ├── ip.rs ├── length.rs ├── must_match.rs ├── nest_all_fields.rs ├── nested.rs ├── non_control.rs ├── range.rs ├── regex.rs ├── required.rs ├── run-pass ├── absolute_path.rs ├── crate_alias.rs ├── custom.rs ├── default_struct_parameters.rs ├── email.rs ├── length.rs ├── lifetime.rs ├── must_match.rs ├── optional_field.rs ├── range.rs ├── regex.rs ├── schema.rs ├── unsupported_field_type.rs ├── url.rs └── use_in_declarative_macros.rs ├── schema.rs ├── schema_args.rs ├── skip.rs ├── unsupported_array.rs └── url.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.rs] 2 | end_of_line = lf 3 | charset = utf-8 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test_validator: 6 | name: Continuous integration 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Extract Rust version from Cargo.toml 11 | run: | 12 | MSRV=$(sed -n 's/^rust-version = "\([^"]*\)"/\1/p' Cargo.toml) 13 | echo "rust-version: $MSRV" 14 | echo "MSRV=$MSRV" >> $GITHUB_ENV 15 | - name: Install Rust 16 | uses: dtolnay/rust-toolchain@stable # actions-rust-lang/setup-rust-toolchain@v1 17 | with: 18 | toolchain: ${{ env.MSRV }} 19 | - name: Cache dependencies 20 | uses: Swatinem/rust-cache@v2 21 | - name: Build System Info 22 | run: rustc --version 23 | - name: Tests 24 | run: | 25 | cargo build --no-default-features 26 | cargo test --no-default-features 27 | cargo build --features derive --features card 28 | cargo test --features derive --features card 29 | test_validator-nightly: 30 | name: Continuous integration 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | include: 35 | - rust: nightly 36 | steps: 37 | - uses: actions/checkout@v3 38 | - name: Install Rust 39 | uses: dtolnay/rust-toolchain@stable 40 | with: 41 | toolchain: ${{ matrix.rust }} 42 | - name: Cache dependencies 43 | uses: Swatinem/rust-cache@v2 44 | - name: Build System Info 45 | run: rustc --version 46 | - name: Tests 47 | run: | 48 | cargo build --no-default-features 49 | cargo test --no-default-features 50 | cargo build --all-features 51 | cargo test --all-features -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.iml 2 | /.idea 3 | target/ 4 | Cargo.lock 5 | testing-bugs/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ## 0.20.0 (2025/01/20) 4 | 5 | - Implement `AsRegex` for `std::sync::LazyLock` 6 | - Bug fix for nested issue with custom only running nested if outer passes 7 | - Support `Deserialize` for `ValidationErrors` 8 | 9 | ## 0.19.0 (2024/11/03) 10 | 11 | - Swap to using proc-macro-error-2 instead of proc-macro-error for Syn 12 | - Bumped MSRV to 1.81 because of error naming changes. 13 | - Add more ValidateRegex impl 14 | 15 | 16 | ## 0.18.1 (2024/04/11) 17 | 18 | - Fix multiple custom validation 19 | - Fix nested error handling 20 | 21 | 22 | ## 0.18.0 (2024/04/05) 23 | 24 | - Fix regressions from the derive rewrite, some things are back to 0.16 (eg custom functions) 25 | - Remove require_nested, use required and nested validators instead 26 | - Always require `nested` on the field for nested validation 27 | 28 | 29 | ## 0.17.0 (2024/03/04) 30 | 31 | - Derive macro has been entirely rewritten 32 | - Validation is now done through traits that you can implement 33 | - Remove phone validator 34 | - Remove automatic use of serde rename for fields name (temporary) 35 | 36 | ## 0.16.0 (2022/06/27) 37 | 38 | - Allow passing code/message to `required` 39 | - Add `does_not_contain` validator 40 | - Check email length before validating it 41 | 42 | ## 0.15.0 (2022/05/03) 43 | 44 | - Allow passing args to schema validator 45 | - Implement HasLen for map/set types 46 | - Remove `validator_types` from validator crate 47 | - Add ValidationErrors::errors_mut 48 | - Ignore unsupported fields rather than erroring 49 | 50 | ## 0.14.0 (2021/06/29) 51 | 52 | - Allow passing arguments to custom functions 53 | - Better `Display` implementation 54 | - Better parsing of schema validation function in derive 55 | 56 | ## 0.13.0 (2021/03/22) 57 | 58 | - Allow multiple schema-level validations 59 | 60 | ## 0.12.0 (2020/11/26) 61 | 62 | - Allow `length` and `range` validators to take a reference to a variable 63 | - Make validator work with `Option>` 64 | - Support reference for length types 65 | - Fix `phone`, `unic` and `card` feature to actually work 66 | 67 | ## 0.11.0 (2020/09/09) 68 | 69 | - Add a `derive` feature so you don't need to add `validator_derive` to your `Cargo.toml` 70 | 71 | ## 0.10.1 (2020/06/09) 72 | 73 | - Add a blanket Validate implementation for references 74 | - Add `Required` and `RequiredNested` validators 75 | 76 | ## 0.10.0 (2019/10/18) 77 | 78 | - Add `non_control_characters` validation 79 | 80 | ## 0.9.0 (2019/05/xx) 81 | 82 | - `ValidationErrors::errors` and `ValidationErrors::field_errors` now use `&self` instead of `self` 83 | - Move to edition 2018 84 | 85 | ## 0.8.0 (2018/09/19) 86 | 87 | - Change error type to allow use with nested validation 88 | 89 | ## 0.7.1 (2018/07/27) 90 | 91 | - Make validators work on `Cow` 92 | 93 | ## 0.7.0 (2018/05/29) 94 | 95 | - Feature gate the card validator 96 | 97 | ## 0.6.2 (2017/11/08) 98 | 99 | - Fix credit card validation being incorrect in enum 100 | 101 | ## 0.6.1 (2017/11/08) 102 | 103 | - Add international phone number and credit card validation 104 | 105 | ## 0.6.0 (2017/08/12) 106 | 107 | - Re-design `ValidationError` and `Validate` trait 108 | 109 | ## 0.11.0 (2020/09/09) 110 | 111 | - Errors in the proc macro attributes will now point to the exact place the error is 112 | 113 | ## 0.10.1 (2020/06/09) 114 | 115 | - Handle `Required` and `RequiredNested` validators 116 | 117 | ## 0.10.0 (2019/10/18) 118 | 119 | - Update syn & quote 120 | - Move to edition 2018 121 | 122 | ## 0.9.0 (2019/05/xx) 123 | 124 | - Use literals in macros now that it's stable -> bumping minimum Rust version to 1.30 125 | 126 | ## 0.8.0 (2018/09/19) 127 | 128 | - Allow nested validation 129 | 130 | ## 0.7.2 (2018/07/27) 131 | 132 | - Make validators work on `Cow` 133 | 134 | ## 0.7.1 (2018/06/28) 135 | 136 | - Update dependencies 137 | 138 | ## 0.7.0 (2018/05/29) 139 | 140 | - Feature gate the card validator 141 | 142 | ## 0.6.5 (2018/04/14) 143 | 144 | - Fix path for regex starting with `::` 145 | - Update syn and quote 146 | 147 | ## 0.6.4 (2018/03/20) 148 | 149 | - Support `Option>` types 150 | 151 | ## 0.6.3 (2018/03/19) 152 | 153 | - Fix path for custom validators starting with `::` 154 | 155 | ## 0.6.2 (2018/03/17) 156 | 157 | - Update syn and quote 158 | 159 | ## 0.6.1 (2017/11/08) 160 | 161 | - Add international phone number and credit card derive 162 | 163 | ## 0.6.0 (2017/08/12) 164 | 165 | - Change generated code to make the new design of errors work 166 | 167 | ## 0.5.0 (2017/05/22) > validator_derive only 168 | 169 | - Fix range validator not working on Option 170 | - Update to serde 1.0 171 | 172 | ## 0.4.1 (2017/02/14) > validator_derive only 173 | 174 | - Fix potential conflicts with other attributes 175 | 176 | ## 0.4.0 (2017/01/30) 177 | 178 | - Validators now work on `Option` field and struct/fields with lifetimes 179 | 180 | ## 0.3.0 (2017/01/17) 181 | 182 | - Add `contains` and `regex` validator 183 | - BREAKING: change `Errors` type to be a newtype in order to extend it 184 | 185 | ## 0.2.0 (2017/01/17) 186 | 187 | - Remove need for `attr_literals` feature 188 | - Fix error when not having validation on each field 189 | - Add struct level validation 190 | - Add `must_match` validator 191 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["validator", "validator_derive", "validator_derive_tests"] 4 | 5 | [workspace.package] 6 | rust-version = "1.81" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vincent Prouillet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | -------------------------------------------------------------------------------- /validator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "validator" 3 | version = "0.20.0" 4 | authors = ["Vincent Prouillet ) -> fmt::Result { 7 | if let Some(msg) = self.message.as_ref() { 8 | write!(fmt, "{}", msg) 9 | } else { 10 | write!(fmt, "Validation error: {} [{:?}]", self.code, self.params) 11 | } 12 | } 13 | } 14 | 15 | fn display_errors( 16 | fmt: &mut fmt::Formatter<'_>, 17 | errs: &ValidationErrorsKind, 18 | path: &str, 19 | ) -> fmt::Result { 20 | fn display_struct( 21 | fmt: &mut fmt::Formatter<'_>, 22 | errs: &ValidationErrors, 23 | path: &str, 24 | ) -> fmt::Result { 25 | let mut full_path = String::new(); 26 | write!(&mut full_path, "{}.", path)?; 27 | let base_len = full_path.len(); 28 | for (path, err) in errs.errors() { 29 | write!(&mut full_path, "{}", path)?; 30 | display_errors(fmt, err, &full_path)?; 31 | full_path.truncate(base_len); 32 | } 33 | Ok(()) 34 | } 35 | 36 | match errs { 37 | ValidationErrorsKind::Field(errs) => { 38 | write!(fmt, "{}: ", path)?; 39 | let len = errs.len(); 40 | for (idx, err) in errs.iter().enumerate() { 41 | if idx + 1 == len { 42 | write!(fmt, "{}", err)?; 43 | } else { 44 | write!(fmt, "{}, ", err)?; 45 | } 46 | } 47 | Ok(()) 48 | } 49 | ValidationErrorsKind::Struct(errs) => display_struct(fmt, errs, path), 50 | ValidationErrorsKind::List(errs) => { 51 | let mut full_path = String::new(); 52 | write!(&mut full_path, "{}", path)?; 53 | let base_len = full_path.len(); 54 | for (idx, err) in errs.iter() { 55 | write!(&mut full_path, "[{}]", idx)?; 56 | display_struct(fmt, err, &full_path)?; 57 | full_path.truncate(base_len); 58 | } 59 | Ok(()) 60 | } 61 | } 62 | } 63 | 64 | impl fmt::Display for ValidationErrors { 65 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | for (idx, (path, err)) in self.errors().iter().enumerate() { 67 | display_errors(fmt, err, path)?; 68 | if idx + 1 < self.errors().len() { 69 | writeln!(fmt)?; 70 | } 71 | } 72 | Ok(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /validator/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Example: 2 | //! 3 | //! ```ignore, no_run 4 | //! use serde::Deserialize; 5 | //! 6 | //! // A trait that the Validate derive will impl 7 | //! use validator::{Validate, ValidationError}; 8 | //! 9 | //! #[derive(Debug, Validate, Deserialize)] 10 | //! struct SignupData { 11 | //! #[validate(email)] 12 | //! mail: String, 13 | //! #[validate(url)] 14 | //! site: String, 15 | //! #[validate(length(min = 1), custom(function = "validate_unique_username"))] 16 | //! #[serde(rename = "firstName")] 17 | //! first_name: String, 18 | //! #[validate(range(min = 18, max = 20))] 19 | //! age: u32, 20 | //! } 21 | //! 22 | //! fn validate_unique_username(username: &str) -> Result<(), ValidationError> { 23 | //! if username == "xXxShad0wxXx" { 24 | //! // the value of the username will automatically be added later 25 | //! return Err(ValidationError::new("terrible_username")); 26 | //! } 27 | //! 28 | //! Ok(()) 29 | //! } 30 | //! 31 | //! match signup_data.validate() { 32 | //! Ok(_) => (), 33 | //! Err(e) => return e; 34 | //! }; 35 | //! ``` 36 | //! 37 | //! # Available Validations: 38 | //! | Validation | Notes | 39 | //! | ----------------------- | ----------------------------------------------------- | 40 | //! | `email` | | 41 | //! | `url` | | 42 | //! | `length` | | 43 | //! | `range` | | 44 | //! | `must_match` | | 45 | //! | `contains` | | 46 | //! | `does_not_contain` | | 47 | //! | `custom` | | 48 | //! | `regex` | | 49 | //! | `credit_card` | (Requires the feature `card` to be enabled) | 50 | //! | `non_control_character` | | 51 | //! | `required` | | 52 | //! 53 | //! [Checkout the project README of an in-depth usage description with examples.](https://github.com/Keats/validator/blob/master/README.md) 54 | //! 55 | //! # Installation: 56 | //! Add the validator to the dependencies in the Cargo.toml file. 57 | //! 58 | //! ```toml 59 | //! [dependencies] 60 | //! validator = { version = "0.16", features = ["derive"] } 61 | //! ``` 62 | 63 | mod display_impl; 64 | mod traits; 65 | mod types; 66 | mod validation; 67 | 68 | #[cfg(feature = "card")] 69 | pub use validation::cards::ValidateCreditCard; 70 | pub use validation::contains::ValidateContains; 71 | pub use validation::does_not_contain::ValidateDoesNotContain; 72 | pub use validation::email::ValidateEmail; 73 | pub use validation::ip::ValidateIp; 74 | pub use validation::length::ValidateLength; 75 | pub use validation::must_match::validate_must_match; 76 | pub use validation::non_control_character::ValidateNonControlCharacter; 77 | pub use validation::range::ValidateRange; 78 | pub use validation::regex::{AsRegex, ValidateRegex}; 79 | pub use validation::required::ValidateRequired; 80 | pub use validation::urls::ValidateUrl; 81 | 82 | pub use traits::{Validate, ValidateArgs}; 83 | pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind}; 84 | 85 | #[cfg(feature = "derive")] 86 | pub use validator_derive::Validate; 87 | -------------------------------------------------------------------------------- /validator/src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{ValidationErrors, ValidationErrorsKind}; 2 | use std::borrow::Cow; 3 | use std::collections::btree_map::BTreeMap; 4 | use std::collections::HashMap; 5 | 6 | /// This is the original trait that was implemented by deriving `Validate`. It will still be 7 | /// implemented for struct validations that don't take custom arguments. The call is being 8 | /// forwarded to the `ValidateArgs<'v_a>` trait. 9 | pub trait Validate { 10 | fn validate(&self) -> Result<(), ValidationErrors>; 11 | } 12 | 13 | impl Validate for &T { 14 | fn validate(&self) -> Result<(), ValidationErrors> { 15 | T::validate(self) 16 | } 17 | } 18 | 19 | macro_rules! impl_validate_list { 20 | ($container:ty) => { 21 | impl Validate for $container { 22 | fn validate(&self) -> Result<(), ValidationErrors> { 23 | let mut vec_err: BTreeMap> = BTreeMap::new(); 24 | 25 | for (index, item) in self.iter().enumerate() { 26 | if let Err(e) = item.validate() { 27 | vec_err.insert(index, Box::new(e)); 28 | } 29 | } 30 | 31 | if vec_err.is_empty() { 32 | Ok(()) 33 | } else { 34 | let err_kind = ValidationErrorsKind::List(vec_err); 35 | let errors = ValidationErrors(std::collections::HashMap::from([( 36 | Cow::Borrowed("_tmp_validator"), 37 | err_kind, 38 | )])); 39 | Err(errors) 40 | } 41 | } 42 | } 43 | }; 44 | } 45 | 46 | impl_validate_list!(std::collections::HashSet); 47 | impl_validate_list!(std::collections::BTreeSet); 48 | impl_validate_list!(std::collections::BinaryHeap); 49 | impl_validate_list!(std::collections::LinkedList); 50 | impl_validate_list!(std::collections::VecDeque); 51 | impl_validate_list!(std::vec::Vec); 52 | impl_validate_list!([T]); 53 | 54 | impl Validate for [T; N] { 55 | fn validate(&self) -> Result<(), ValidationErrors> { 56 | let mut vec_err: BTreeMap> = BTreeMap::new(); 57 | 58 | for (index, item) in self.iter().enumerate() { 59 | if let Err(e) = item.validate() { 60 | vec_err.insert(index, Box::new(e)); 61 | } 62 | } 63 | 64 | if vec_err.is_empty() { 65 | Ok(()) 66 | } else { 67 | let err_kind = ValidationErrorsKind::List(vec_err); 68 | let errors = ValidationErrors(std::collections::HashMap::from([( 69 | Cow::Borrowed("_tmp_validator"), 70 | err_kind, 71 | )])); 72 | Err(errors) 73 | } 74 | } 75 | } 76 | 77 | impl Validate for &HashMap { 78 | fn validate(&self) -> Result<(), ValidationErrors> { 79 | let mut vec_err: BTreeMap> = BTreeMap::new(); 80 | 81 | for (index, (_key, value)) in self.iter().enumerate() { 82 | if let Err(e) = value.validate() { 83 | vec_err.insert(index, Box::new(e)); 84 | } 85 | } 86 | 87 | if vec_err.is_empty() { 88 | Ok(()) 89 | } else { 90 | let err_kind = ValidationErrorsKind::List(vec_err); 91 | let errors = 92 | ValidationErrors(HashMap::from([(Cow::Borrowed("_tmp_validator"), err_kind)])); 93 | Err(errors) 94 | } 95 | } 96 | } 97 | 98 | impl Validate for &BTreeMap { 99 | fn validate(&self) -> Result<(), ValidationErrors> { 100 | let mut vec_err: BTreeMap> = BTreeMap::new(); 101 | 102 | for (index, (_key, value)) in self.iter().enumerate() { 103 | if let Err(e) = value.validate() { 104 | vec_err.insert(index, Box::new(e)); 105 | } 106 | } 107 | 108 | if vec_err.is_empty() { 109 | Ok(()) 110 | } else { 111 | let err_kind = ValidationErrorsKind::List(vec_err); 112 | let errors = 113 | ValidationErrors(HashMap::from([(Cow::Borrowed("_tmp_validator"), err_kind)])); 114 | Err(errors) 115 | } 116 | } 117 | } 118 | 119 | /// This trait will be implemented by deriving `Validate`. This implementation can take one 120 | /// argument and pass this on to custom validators. The default `Args` type will be `()` if 121 | /// there is no custom validation with defined arguments. 122 | /// 123 | /// The `Args` type can use the lifetime `'v_a` to pass references onto the validator. 124 | pub trait ValidateArgs<'v_a> { 125 | type Args; 126 | fn validate_with_args(&self, args: Self::Args) -> Result<(), ValidationErrors>; 127 | } 128 | 129 | impl<'v_a, T, U> ValidateArgs<'v_a> for Option 130 | where 131 | T: ValidateArgs<'v_a, Args = U>, 132 | { 133 | type Args = U; 134 | 135 | fn validate_with_args(&self, args: Self::Args) -> Result<(), ValidationErrors> { 136 | if let Some(nested) = self { 137 | T::validate_with_args(nested, args) 138 | } else { 139 | Ok(()) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /validator/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::{hash_map::Entry::Vacant, BTreeMap, HashMap}; 3 | 4 | use serde::ser::Serialize; 5 | use serde_derive::{Deserialize, Serialize}; 6 | use serde_json::{to_value, Value}; 7 | 8 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] 9 | pub struct ValidationError { 10 | pub code: Cow<'static, str>, 11 | pub message: Option>, 12 | pub params: HashMap, Value>, 13 | } 14 | 15 | impl ValidationError { 16 | pub fn new(code: &'static str) -> ValidationError { 17 | ValidationError { code: Cow::from(code), message: None, params: HashMap::new() } 18 | } 19 | 20 | pub fn add_param(&mut self, name: Cow<'static, str>, val: &T) { 21 | self.params.insert(name, to_value(val).unwrap()); 22 | } 23 | 24 | /// Adds a custom message to a `ValidationError` that will be used when displaying the 25 | /// `ValidationError`, instead of an auto-generated description. 26 | pub fn with_message(mut self, message: Cow<'static, str>) -> ValidationError { 27 | self.message = Some(message); 28 | self 29 | } 30 | } 31 | 32 | impl std::error::Error for ValidationError { 33 | fn description(&self) -> &str { 34 | &self.code 35 | } 36 | fn cause(&self) -> Option<&dyn std::error::Error> { 37 | None 38 | } 39 | } 40 | 41 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 42 | #[serde(untagged)] 43 | pub enum ValidationErrorsKind { 44 | Struct(Box), 45 | List(BTreeMap>), 46 | Field(Vec), 47 | } 48 | 49 | #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)] 50 | pub struct ValidationErrors(pub HashMap, ValidationErrorsKind>); 51 | 52 | impl ValidationErrors { 53 | pub fn new() -> ValidationErrors { 54 | ValidationErrors(HashMap::new()) 55 | } 56 | 57 | /// Returns a boolean indicating whether a validation result includes validation errors for a 58 | /// given field. May be used as a condition for performing nested struct validations on a field 59 | /// in the absence of field-level validation errors. 60 | #[must_use] 61 | pub fn has_error(result: &Result<(), ValidationErrors>, field: &'static str) -> bool { 62 | match result { 63 | Ok(()) => false, 64 | Err(ref errs) => errs.contains_key(field), 65 | } 66 | } 67 | 68 | pub fn merge_self( 69 | &mut self, 70 | field: &'static str, 71 | child: Result<(), ValidationErrors>, 72 | ) -> &mut ValidationErrors { 73 | match child { 74 | Ok(()) => self, 75 | Err(mut errors) => { 76 | // This is a bit of a hack to be able to support collections which return a 77 | // `ValidationErrors` with a made-up `_tmp_validator` entry which we need to strip 78 | // off. 79 | if let Some(collection) = errors.0.remove("_tmp_validator") { 80 | self.add_nested(field, collection); 81 | } else { 82 | self.add_nested(field, ValidationErrorsKind::Struct(Box::new(errors))); 83 | } 84 | 85 | self 86 | } 87 | } 88 | } 89 | 90 | /// Returns the combined outcome of a struct's validation result along with the nested 91 | /// validation result for one of its fields. 92 | pub fn merge( 93 | parent: Result<(), ValidationErrors>, 94 | field: &'static str, 95 | child: Result<(), ValidationErrors>, 96 | ) -> Result<(), ValidationErrors> { 97 | match child { 98 | Ok(()) => parent, 99 | Err(errors) => { 100 | parent.and_then(|_| Err(ValidationErrors::new())).map_err(|mut parent_errors| { 101 | parent_errors.add_nested(field, ValidationErrorsKind::Struct(Box::new(errors))); 102 | parent_errors 103 | }) 104 | } 105 | } 106 | } 107 | 108 | /// Returns the combined outcome of a struct's validation result along with the nested 109 | /// validation result for one of its fields where that field is a vector of validating structs. 110 | pub fn merge_all( 111 | parent: Result<(), ValidationErrors>, 112 | field: &'static str, 113 | children: Vec>, 114 | ) -> Result<(), ValidationErrors> { 115 | let errors = children 116 | .into_iter() 117 | .enumerate() 118 | .filter_map(|(i, res)| res.err().map(|mut err| (i, err.remove(field)))) 119 | .filter_map(|(i, entry)| match entry { 120 | Some(ValidationErrorsKind::Struct(errors)) => Some((i, errors)), 121 | _ => None, 122 | }) 123 | .collect::>(); 124 | 125 | if errors.is_empty() { 126 | parent 127 | } else { 128 | parent.and_then(|_| Err(ValidationErrors::new())).map_err(|mut parent_errors| { 129 | parent_errors.add_nested(field, ValidationErrorsKind::List(errors)); 130 | parent_errors 131 | }) 132 | } 133 | } 134 | 135 | /// Returns a map of field-level validation errors found for the struct that was validated and 136 | /// any of it's nested structs that are tagged for validation. 137 | pub fn errors(&self) -> &HashMap, ValidationErrorsKind> { 138 | &self.0 139 | } 140 | 141 | /// Returns a mutable map of field-level validation errors found for the struct that was validated and 142 | /// any of it's nested structs that are tagged for validation. 143 | pub fn errors_mut(&mut self) -> &mut HashMap, ValidationErrorsKind> { 144 | &mut self.0 145 | } 146 | 147 | /// Consume the struct, returning the validation errors found 148 | pub fn into_errors(self) -> HashMap, ValidationErrorsKind> { 149 | self.0 150 | } 151 | 152 | /// Returns a map of only field-level validation errors found for the struct that was validated. 153 | pub fn field_errors(&self) -> HashMap, &Vec> { 154 | self.0 155 | .iter() 156 | .filter_map(|(k, v)| { 157 | if let ValidationErrorsKind::Field(errors) = v { 158 | Some((k.clone(), errors)) 159 | } else { 160 | None 161 | } 162 | }) 163 | .collect::>() 164 | } 165 | 166 | pub fn add(&mut self, field: &'static str, error: ValidationError) { 167 | if let ValidationErrorsKind::Field(ref mut vec) = self 168 | .0 169 | .entry(Cow::Borrowed(field)) 170 | .or_insert_with(|| ValidationErrorsKind::Field(vec![])) 171 | { 172 | vec.push(error); 173 | } else { 174 | panic!("Attempt to add field validation to a non-Field ValidationErrorsKind instance"); 175 | } 176 | } 177 | 178 | #[must_use] 179 | pub fn is_empty(&self) -> bool { 180 | self.0.is_empty() 181 | } 182 | 183 | fn add_nested(&mut self, field: &'static str, errors: ValidationErrorsKind) { 184 | if let Vacant(entry) = self.0.entry(Cow::Borrowed(field)) { 185 | entry.insert(errors); 186 | } else { 187 | panic!("Attempt to replace non-empty ValidationErrors entry"); 188 | } 189 | } 190 | 191 | #[must_use] 192 | fn contains_key(&self, field: &'static str) -> bool { 193 | self.0.contains_key(field) 194 | } 195 | 196 | fn remove(&mut self, field: &'static str) -> Option { 197 | self.0.remove(field) 198 | } 199 | } 200 | 201 | impl std::error::Error for ValidationErrors { 202 | fn description(&self) -> &str { 203 | "Validation failed" 204 | } 205 | fn cause(&self) -> Option<&dyn std::error::Error> { 206 | None 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /validator/src/validation/cards.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[cfg(feature = "card")] 4 | use card_validate::Validate as CardValidate; 5 | 6 | pub trait ValidateCreditCard { 7 | fn validate_credit_card(&self) -> bool { 8 | let card_string = self.as_credit_card_string(); 9 | CardValidate::from(&card_string).is_ok() 10 | } 11 | 12 | fn as_credit_card_string(&self) -> Cow; 13 | } 14 | 15 | impl> ValidateCreditCard for T { 16 | fn as_credit_card_string(&self) -> Cow { 17 | Cow::from(self.as_ref()) 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | #[cfg(feature = "card")] 23 | mod tests { 24 | use super::ValidateCreditCard; 25 | use std::borrow::Cow; 26 | 27 | #[test] 28 | fn test_credit_card() { 29 | let tests = vec![ 30 | ("4539571147647251", true), 31 | ("343380440754432", true), 32 | ("zduhefljsdfKJKJZHUI", false), 33 | ("5236313877109141", false), 34 | ]; 35 | 36 | for (input, expected) in tests { 37 | assert_eq!(input.validate_credit_card(), expected); 38 | } 39 | } 40 | 41 | #[test] 42 | fn test_credit_card_cow() { 43 | let test: Cow<'static, str> = "4539571147647251".into(); 44 | assert!(test.validate_credit_card()); 45 | let test: Cow<'static, str> = String::from("4539571147647251").into(); 46 | assert!(test.validate_credit_card()); 47 | let test: Cow<'static, str> = "5236313877109141".into(); 48 | assert!(!test.validate_credit_card()); 49 | let test: Cow<'static, str> = String::from("5236313877109141").into(); 50 | assert!(!test.validate_credit_card()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /validator/src/validation/contains.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | use std::hash::BuildHasher; 4 | 5 | pub trait ValidateContains { 6 | fn validate_contains(&self, needle: &str) -> bool; 7 | } 8 | 9 | impl ValidateContains for String { 10 | fn validate_contains(&self, needle: &str) -> bool { 11 | self.contains(needle) 12 | } 13 | } 14 | 15 | impl ValidateContains for Option 16 | where 17 | T: ValidateContains, 18 | { 19 | fn validate_contains(&self, needle: &str) -> bool { 20 | if let Some(v) = self { 21 | v.validate_contains(needle) 22 | } else { 23 | true 24 | } 25 | } 26 | } 27 | 28 | impl ValidateContains for &T 29 | where 30 | T: ValidateContains, 31 | { 32 | fn validate_contains(&self, needle: &str) -> bool { 33 | T::validate_contains(self, needle) 34 | } 35 | } 36 | 37 | impl ValidateContains for Cow<'_, T> 38 | where 39 | T: ToOwned + ?Sized, 40 | for<'a> &'a T: ValidateContains, 41 | { 42 | fn validate_contains(&self, needle: &str) -> bool { 43 | self.as_ref().validate_contains(needle) 44 | } 45 | } 46 | 47 | impl ValidateContains for &str { 48 | fn validate_contains(&self, needle: &str) -> bool { 49 | self.contains(needle) 50 | } 51 | } 52 | 53 | impl ValidateContains for HashMap { 54 | fn validate_contains(&self, needle: &str) -> bool { 55 | self.contains_key(needle) 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | #[test] 64 | fn test_validate_contains_string() { 65 | assert!("hey".validate_contains("e")); 66 | } 67 | 68 | #[test] 69 | fn test_validate_contains_string_can_fail() { 70 | assert!(!"hey".validate_contains("o")); 71 | } 72 | 73 | #[test] 74 | fn test_validate_contains_hashmap_key() { 75 | let mut map = HashMap::new(); 76 | map.insert("hey".to_string(), 1); 77 | assert!(map.validate_contains("hey")); 78 | } 79 | 80 | #[test] 81 | fn test_validate_contains_hashmap_key_can_fail() { 82 | let mut map = HashMap::new(); 83 | map.insert("hey".to_string(), 1); 84 | assert!(!map.validate_contains("bob")); 85 | } 86 | 87 | #[test] 88 | fn test_validate_contains_cow() { 89 | let test: Cow<'static, str> = "hey".into(); 90 | assert!(test.validate_contains("e")); 91 | let test: Cow<'static, str> = String::from("hey").into(); 92 | assert!(test.validate_contains("e")); 93 | } 94 | 95 | #[test] 96 | fn test_validate_contains_cow_can_fail() { 97 | let test: Cow<'static, str> = "hey".into(); 98 | assert!(!test.validate_contains("o")); 99 | let test: Cow<'static, str> = String::from("hey").into(); 100 | assert!(!test.validate_contains("o")); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /validator/src/validation/does_not_contain.rs: -------------------------------------------------------------------------------- 1 | use crate::ValidateContains; 2 | 3 | pub trait ValidateDoesNotContain { 4 | fn validate_does_not_contain(&self, needle: &str) -> bool; 5 | } 6 | 7 | impl ValidateDoesNotContain for T 8 | where 9 | T: ValidateContains, 10 | { 11 | fn validate_does_not_contain(&self, needle: &str) -> bool { 12 | !self.validate_contains(needle) 13 | } 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use std::borrow::Cow; 19 | use std::collections::HashMap; 20 | 21 | use super::*; 22 | 23 | #[test] 24 | fn test_validate_does_not_contain_string() { 25 | assert!("hey".validate_does_not_contain("g")); 26 | } 27 | 28 | #[test] 29 | fn test_validate_does_not_contain_string_can_fail() { 30 | assert!(!"hey".validate_does_not_contain("e")); 31 | } 32 | 33 | #[test] 34 | fn test_validate_does_not_contain_hashmap_key() { 35 | let mut map = HashMap::new(); 36 | map.insert("hey".to_string(), 1); 37 | assert!(map.validate_does_not_contain("bob")); 38 | } 39 | 40 | #[test] 41 | fn test_validate_does_not_contain_hashmap_key_can_fail() { 42 | let mut map = HashMap::new(); 43 | map.insert("hey".to_string(), 1); 44 | assert!(!map.validate_does_not_contain("hey")); 45 | } 46 | 47 | #[test] 48 | fn test_validate_does_not_contain_cow() { 49 | let test: Cow<'static, str> = "hey".into(); 50 | assert!(test.validate_does_not_contain("b")); 51 | let test: Cow<'static, str> = String::from("hey").into(); 52 | assert!(test.validate_does_not_contain("b")); 53 | } 54 | 55 | #[test] 56 | fn test_validate_does_not_contain_cow_can_fail() { 57 | let test: Cow<'static, str> = "hey".into(); 58 | assert!(!test.validate_does_not_contain("e")); 59 | let test: Cow<'static, str> = String::from("hey").into(); 60 | assert!(!test.validate_does_not_contain("e")); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /validator/src/validation/email.rs: -------------------------------------------------------------------------------- 1 | use idna::domain_to_ascii; 2 | use regex::Regex; 3 | use std::{borrow::Cow, sync::LazyLock}; 4 | 5 | use crate::ValidateIp; 6 | 7 | // Regex from the specs 8 | // https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address 9 | // It will mark esoteric email addresses like quoted string as invalid 10 | static EMAIL_USER_RE: LazyLock = 11 | LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap()); 12 | static EMAIL_DOMAIN_RE: LazyLock = LazyLock::new(|| { 13 | Regex::new( 14 | r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" 15 | ).unwrap() 16 | }); 17 | // literal form, ipv4 or ipv6 address (SMTP 4.1.3) 18 | static EMAIL_LITERAL_RE: LazyLock = 19 | LazyLock::new(|| Regex::new(r"\[([a-fA-F0-9:\.]+)\]\z").unwrap()); 20 | 21 | /// Checks if the domain is a valid domain and if not, check whether it's an IP 22 | #[must_use] 23 | fn validate_domain_part(domain_part: &str) -> bool { 24 | if EMAIL_DOMAIN_RE.is_match(domain_part) { 25 | return true; 26 | } 27 | 28 | // maybe we have an ip as a domain? 29 | match EMAIL_LITERAL_RE.captures(domain_part) { 30 | Some(caps) => match caps.get(1) { 31 | Some(c) => c.as_str().validate_ip(), 32 | None => false, 33 | }, 34 | None => false, 35 | } 36 | } 37 | 38 | /// Validates whether the given string is an email based on the [HTML5 spec](https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address). 39 | /// [RFC 5322](https://tools.ietf.org/html/rfc5322) is not practical in most circumstances and allows email addresses 40 | /// that are unfamiliar to most users. 41 | pub trait ValidateEmail { 42 | fn validate_email(&self) -> bool { 43 | let val = if let Some(v) = self.as_email_string() { 44 | v 45 | } else { 46 | return true; 47 | }; 48 | 49 | if val.is_empty() || !val.contains('@') { 50 | return false; 51 | } 52 | 53 | let parts: Vec<&str> = val.rsplitn(2, '@').collect(); 54 | let user_part = parts[1]; 55 | let domain_part = parts[0]; 56 | 57 | // validate the length of each part of the email, BEFORE doing the regex 58 | // according to RFC5321 the max length of the local part is 64 characters 59 | // and the max length of the domain part is 255 characters 60 | // https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1 61 | if user_part.chars().count() > 64 || domain_part.chars().count() > 255 { 62 | return false; 63 | } 64 | 65 | if !EMAIL_USER_RE.is_match(user_part) { 66 | return false; 67 | } 68 | 69 | if !validate_domain_part(domain_part) { 70 | // Still the possibility of an [IDN](https://en.wikipedia.org/wiki/Internationalized_domain_name) 71 | return match domain_to_ascii(domain_part) { 72 | Ok(d) => validate_domain_part(&d), 73 | Err(_) => false, 74 | }; 75 | } 76 | 77 | true 78 | } 79 | 80 | fn as_email_string(&self) -> Option>; 81 | } 82 | 83 | impl ValidateEmail for &T 84 | where 85 | T: ValidateEmail, 86 | { 87 | fn as_email_string(&self) -> Option> { 88 | T::as_email_string(self) 89 | } 90 | } 91 | 92 | impl ValidateEmail for String { 93 | fn as_email_string(&self) -> Option> { 94 | Some(Cow::from(self)) 95 | } 96 | } 97 | 98 | impl ValidateEmail for Option 99 | where 100 | T: ValidateEmail, 101 | { 102 | fn as_email_string(&self) -> Option> { 103 | let Some(u) = self else { 104 | return None; 105 | }; 106 | 107 | T::as_email_string(u) 108 | } 109 | } 110 | 111 | impl ValidateEmail for &str { 112 | fn as_email_string(&self) -> Option> { 113 | Some(Cow::from(*self)) 114 | } 115 | } 116 | 117 | impl ValidateEmail for Cow<'_, str> { 118 | fn as_email_string(&self) -> Option> { 119 | Some(self.clone()) 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use std::borrow::Cow; 126 | 127 | use crate::ValidateEmail; 128 | 129 | #[test] 130 | fn test_validate_email() { 131 | // Test cases taken from Django 132 | // https://github.com/django/django/blob/master/tests/validators/tests.py#L48 133 | let tests = vec![ 134 | ("email@here.com", true), 135 | ("weirder-email@here.and.there.com", true), 136 | (r#"!def!xyz%abc@example.com"#, true), 137 | ("email@[127.0.0.1]", true), 138 | ("email@[2001:dB8::1]", true), 139 | ("email@[2001:dB8:0:0:0:0:0:1]", true), 140 | ("email@[::fffF:127.0.0.1]", true), 141 | ("example@valid-----hyphens.com", true), 142 | ("example@valid-with-hyphens.com", true), 143 | ("test@domain.with.idn.tld.उदाहरण.परीक्षा", true), 144 | (r#""test@test"@example.com"#, false), 145 | // max length for domain name labels is 63 characters per RFC 1034 146 | ("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true), 147 | ("a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.atm", true), 148 | ( 149 | "a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbb.atm", 150 | true, 151 | ), 152 | // 64 * a 153 | ("a@atm.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false), 154 | ("", false), 155 | ("abc", false), 156 | ("abc@", false), 157 | ("abc@bar", true), 158 | ("a @x.cz", false), 159 | ("abc@.com", false), 160 | ("something@@somewhere.com", false), 161 | ("email@127.0.0.1", true), 162 | ("email@[127.0.0.256]", false), 163 | ("email@[2001:db8::12345]", false), 164 | ("email@[2001:db8:0:0:0:0:1]", false), 165 | ("email@[::ffff:127.0.0.256]", false), 166 | ("example@invalid-.com", false), 167 | ("example@-invalid.com", false), 168 | ("example@invalid.com-", false), 169 | ("example@inv-.alid-.com", false), 170 | ("example@inv-.-alid.com", false), 171 | (r#"test@example.com\n\n