├── .github └── workflows │ └── general.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── derive ├── Cargo.toml └── src │ ├── lib.rs │ └── parsing.rs ├── examples └── derive.rs └── src ├── lib.rs ├── prelude.rs ├── rules ├── all.rs ├── and.rs ├── any.rs ├── eval.rs ├── is_in.rs ├── min_max_length.rs ├── min_max_range.rs ├── min_max_size.rs ├── mod.rs ├── opt.rs ├── or.rs ├── regex.rs └── validate.rs └── valid.rs /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | profile: minimal 16 | toolchain: stable 17 | override: true 18 | - uses: actions-rs/cargo@v1 19 | with: 20 | command: test 21 | args: --all-features 22 | fmt: 23 | name: Rustfmt 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | override: true 32 | - run: rustup component add rustfmt 33 | - uses: actions-rs/cargo@v1 34 | with: 35 | command: fmt 36 | args: --all -- --check 37 | clippy: 38 | name: Clippy 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | profile: minimal 45 | toolchain: stable 46 | override: true 47 | - run: rustup component add clippy 48 | - uses: actions-rs/cargo@v1 49 | with: 50 | command: clippy 51 | args: -- -D warnings -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Project and editor files 4 | .vscode/ 5 | .idea/ 6 | *.vim 7 | *.vi -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 19 | 20 | [[package]] 21 | name = "chrono" 22 | version = "0.4.19" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 25 | dependencies = [ 26 | "libc", 27 | "num-integer", 28 | "num-traits", 29 | "time", 30 | "winapi", 31 | ] 32 | 33 | [[package]] 34 | name = "claim" 35 | version = "0.5.0" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "f81099d6bb72e1df6d50bb2347224b666a670912bb7f06dbe867a4a070ab3ce8" 38 | dependencies = [ 39 | "autocfg", 40 | ] 41 | 42 | [[package]] 43 | name = "itoa" 44 | version = "1.0.2" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 47 | 48 | [[package]] 49 | name = "libc" 50 | version = "0.2.125" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" 53 | 54 | [[package]] 55 | name = "memchr" 56 | version = "2.4.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 59 | 60 | [[package]] 61 | name = "num-integer" 62 | version = "0.1.45" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 65 | dependencies = [ 66 | "autocfg", 67 | "num-traits", 68 | ] 69 | 70 | [[package]] 71 | name = "num-traits" 72 | version = "0.2.15" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 75 | dependencies = [ 76 | "autocfg", 77 | ] 78 | 79 | [[package]] 80 | name = "proc-macro2" 81 | version = "1.0.40" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 84 | dependencies = [ 85 | "unicode-ident", 86 | ] 87 | 88 | [[package]] 89 | name = "quote" 90 | version = "1.0.10" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 93 | dependencies = [ 94 | "proc-macro2", 95 | ] 96 | 97 | [[package]] 98 | name = "regex" 99 | version = "1.5.4" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 102 | dependencies = [ 103 | "aho-corasick", 104 | "memchr", 105 | "regex-syntax", 106 | ] 107 | 108 | [[package]] 109 | name = "regex-syntax" 110 | version = "0.6.25" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 113 | 114 | [[package]] 115 | name = "ryu" 116 | version = "1.0.10" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 119 | 120 | [[package]] 121 | name = "serde" 122 | version = "1.0.137" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" 125 | 126 | [[package]] 127 | name = "serde_derive" 128 | version = "1.0.137" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" 131 | dependencies = [ 132 | "proc-macro2", 133 | "quote", 134 | "syn", 135 | ] 136 | 137 | [[package]] 138 | name = "serde_json" 139 | version = "1.0.81" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" 142 | dependencies = [ 143 | "itoa", 144 | "ryu", 145 | "serde", 146 | ] 147 | 148 | [[package]] 149 | name = "syn" 150 | version = "1.0.98" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 153 | dependencies = [ 154 | "proc-macro2", 155 | "quote", 156 | "unicode-ident", 157 | ] 158 | 159 | [[package]] 160 | name = "time" 161 | version = "0.1.44" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 164 | dependencies = [ 165 | "libc", 166 | "wasi", 167 | "winapi", 168 | ] 169 | 170 | [[package]] 171 | name = "type-rules" 172 | version = "0.2.3" 173 | dependencies = [ 174 | "chrono", 175 | "claim", 176 | "regex", 177 | "serde", 178 | "serde_derive", 179 | "serde_json", 180 | "type-rules-derive", 181 | ] 182 | 183 | [[package]] 184 | name = "type-rules-derive" 185 | version = "0.2.3" 186 | dependencies = [ 187 | "proc-macro2", 188 | "quote", 189 | "syn", 190 | ] 191 | 192 | [[package]] 193 | name = "unicode-ident" 194 | version = "1.0.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 197 | 198 | [[package]] 199 | name = "wasi" 200 | version = "0.10.0+wasi-snapshot-preview1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 203 | 204 | [[package]] 205 | name = "winapi" 206 | version = "0.3.9" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 209 | dependencies = [ 210 | "winapi-i686-pc-windows-gnu", 211 | "winapi-x86_64-pc-windows-gnu", 212 | ] 213 | 214 | [[package]] 215 | name = "winapi-i686-pc-windows-gnu" 216 | version = "0.4.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 219 | 220 | [[package]] 221 | name = "winapi-x86_64-pc-windows-gnu" 222 | version = "0.4.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 225 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [".", "derive"] 3 | 4 | [package] 5 | name = "type-rules" 6 | version = "0.2.3" 7 | readme = "README.md" 8 | edition = "2021" 9 | description = "A crate to easily constrain a struct" 10 | homepage = "https://github.com/TheoOiry/type-rules" 11 | repository = "https://github.com/TheoOiry/type-rules" 12 | documentation = "https://docs.rs/type-rules" 13 | keywords = ["rule", "type", "contraint", "restrict", "validate"] 14 | categories = ["data-structures"] 15 | license = "MIT OR Apache-2.0" 16 | authors = ["Théo Oiry "] 17 | 18 | [package.metadata.docs.rs] 19 | features = ["regex", "serde"] 20 | rustdoc-args = ["--cfg", "docsrs"] 21 | 22 | [dependencies] 23 | type-rules-derive = { optional = true, path = "derive", version = "0.2.3" } 24 | regex = { optional = true, version = "1.5.4" } 25 | serde = { optional = true, version = "1.0" } 26 | 27 | [dev-dependencies] 28 | claim = "0.5.0" 29 | chrono = "0.4.19" 30 | serde_json = "1.0.81" 31 | serde_derive = "1.0.137" 32 | 33 | [features] 34 | derive = ["type-rules-derive"] 35 | 36 | [[example]] 37 | name = "derive" 38 | path = "examples/derive.rs" 39 | required-features = [ 40 | "derive", 41 | "regex" 42 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # type-rules 2 | 3 |
4 | 5 | Crates.io version 7 | 8 | 9 | docs.rs docs 11 | 12 | 13 | License 15 | 16 |
17 | 18 | A tool to easily constrain a struct and recover errors. 19 | 20 | ## Table of Contents 21 | 22 | 1. [Install](#install) 23 | 2. [Basic checking](#basic-checking) 24 | 3. [Advanced checking](#advanced-checking) 25 | 4. [Make your own rule](#make-your-own-rule) 26 | 5. [Rules list](#rules-list) 27 | 28 | ## Install 29 | 30 | ```toml 31 | # Cargo.toml 32 | [dependencies] 33 | type-rules = { version = "0.2.3", features = ["derive", "regex", "serde"] } 34 | ``` 35 | 36 | ## Basic checking 37 | 38 | You can declare a struct and impose some constraints on each field 39 | and check the validity like this: 40 | 41 | ```rust 42 | use chrono::prelude::*; 43 | use type_rules::prelude::*; 44 | 45 | #[derive(Validator)] 46 | struct NewUser { 47 | #[rule(MaxLength(100), RegEx(r"^\S+@\S+\.\S+"))] 48 | email: String, 49 | #[rule(MinMaxLength(8, 50))] 50 | password: String, 51 | #[rule(Opt(MaxRange(Utc::now())))] 52 | birth_date: Option> 53 | } 54 | 55 | let new_user = NewUser { 56 | email: "examples@examples.com".to_string(), 57 | password: "OPw$5%hJ".to_string(), 58 | birth_date: None, 59 | }; 60 | assert!(new_user.check_validity().is_ok()); 61 | let new_user = NewUser { 62 | email: "examples@examples.com".to_string(), 63 | password: "O".to_string(), 64 | birth_date: None, 65 | }; 66 | assert!(new_user.check_validity().is_err()); //Value is too short 67 | ``` 68 | 69 | Also works with enums : 70 | 71 | ```rust 72 | use type_rules::prelude::*; 73 | 74 | #[derive(Validator)] 75 | enum MyEnum { 76 | Option1(#[rule(MaxLength(200))] String), 77 | Option2 { 78 | #[rule(MinMaxRange(1, 10))] 79 | integer: u32 80 | }, 81 | Option3, 82 | } 83 | ``` 84 | 85 | ## Advanced checking 86 | 87 | To check recursively, you can use the `Validate` rule 88 | 89 | ```rust 90 | use type_rules::prelude::*; 91 | 92 | #[derive(Validator)] 93 | struct EmailWrapper(#[rule(MaxLength(100), RegEx(r"^\S+@\S+\.\S+"))] String); 94 | 95 | #[derive(Validator)] 96 | struct User { 97 | #[rule(Validate())] 98 | email: EmailWrapper, 99 | #[rule(MinMaxLength(8, 50))] 100 | password: String, 101 | } 102 | ``` 103 | 104 | You can use expressions directly in rule derive attribute. 105 | 106 | For example, you can use const or function directly in the rule parameters: 107 | 108 | ```rust 109 | use type_rules::prelude::*; 110 | use chrono::prelude::*; 111 | 112 | #[derive(Validator)] 113 | struct BirthDate(#[rule(MaxRange(Utc::now()))] DateTime); 114 | ``` 115 | 116 | ```rust 117 | use type_rules::prelude::*; 118 | 119 | #[derive(Validator)] 120 | struct Range { 121 | #[rule(MaxRange(self.max))] 122 | min: u32, 123 | #[rule(MinRange(self.min))] 124 | max: u32, 125 | }; 126 | ``` 127 | 128 | Or use expressions to express a rule directly. 129 | Here is an example of using a rule with more complex values: 130 | 131 | ```rust 132 | use std::env; 133 | use type_rules::prelude::*; 134 | 135 | fn generate_max_payload_rule() -> MaxLength { 136 | MaxLength(match env::var("MAX_PAYLOAD") { 137 | Ok(val) => val.parse().unwrap_or_else(|_| 10000), 138 | Err(_) => 10000, 139 | }) 140 | } 141 | 142 | #[derive(Validator)] 143 | struct Payload(#[rule(generate_max_payload_rule())] String); 144 | ``` 145 | 146 | In this case the `generate_max_payload_rule` function is executed at each check 147 | 148 | ## Make your own rule 149 | 150 | If you need a specific rule, just make a tuple struct (or struct if you make the declaration outside the struct 151 | definition) 152 | that implements the `Rule` feature : 153 | 154 | ```rust 155 | use type_rules::prelude::*; 156 | 157 | struct IsEven(); 158 | 159 | impl Rule for IsEven { 160 | fn check(&self, value: &i32) -> Result<(), String> { 161 | if value % 2 == 0 { 162 | Ok(()) 163 | } else { 164 | Err("Value is not even".into()) 165 | } 166 | } 167 | } 168 | 169 | #[derive(Validator)] 170 | struct MyInteger(#[rule(IsEven())] i32); 171 | ``` 172 | 173 | ## Valid wrapper 174 | 175 | `Valid` is a wrapper for any type that implements `Validator` 176 | it permit to ensure at compile time that the inner type as been 177 | verified. 178 | 179 | With the `serde` feature, Valid can be serialized and deserialized 180 | with validity check. 181 | ```rust 182 | use type_rules::prelude::*; 183 | 184 | #[derive(Validator)] 185 | struct NewUser { 186 | #[rule(MinMaxLength(3, 50))] 187 | username: String, 188 | #[rule(MinMaxLength(8, 100))] 189 | password: String, 190 | } 191 | 192 | fn do_something(user: Valid) { 193 | // No need to check if user is valid 194 | } 195 | 196 | let new_user = NewUser { 197 | username: "example".to_string(), 198 | password: "OPw$5%hJJ".to_string(), 199 | }; 200 | do_something(Valid::new(new_user).unwrap()); 201 | ``` 202 | 203 | ## Rules list 204 | 205 | Here a list of the rules you can find in this crate. 206 | 207 | Each rule has its own [documentation](https://docs.rs/type-rules/latest/type_rules/rules/index.html) 208 | with examples. 209 | 210 | Check the length of any type that implements `AsRef` such 211 | as `String` or `&str`: 212 | 213 | - `MinLength`: Minimum length ex: `MinLength(5)` 214 | - `MaxLength`: Maximum length ex: `MaxLength(20)` 215 | - `MinMaxLength`: Minimum and maximum length ex: `MinMaxLength(5, 20)` 216 | 217 | Check the range for anything that implements `PartialOrd` like all numeric/floating types 218 | or dates with `chrono`: 219 | 220 | - `MinRange`: Minimum range ex: `MinRange(5)` 221 | - `MaxRange`: Maximum range ex: `MaxRange(20)` 222 | - `MinMaxRange`: Minimum and maximum range ex: `MinMaxRange(5, 20)` 223 | 224 | Check the size of a `Vec` : 225 | 226 | - `MinSize`: Minimum size ex: `MinSize(5)` 227 | - `MaxSize`: Maximum size ex: `MaxSize(20)` 228 | - `MinMaxSize`: Minimum and maximum size ex: `MinMaxSize(5, 20)` 229 | 230 | others : 231 | 232 | - `Opt`: Apply another rule to inner value of an `Option` ex: `Opt(MinMaxRange(1, 4))` 233 | - `And`: Rule to ensure that 2 other rules are `Ok` ex: `And(MaxLength(1000), RegEx(r"^\S+@\S+\.\S+"))` 234 | - `Or`: Rule to apply an Or condition on two other rules. ex: `Or(MaxRange(-1), MinRange(1))` 235 | - `Eval`: Rule to constrain any type to a predicate ex: `Eval(predicate, "Error message")` 236 | - `Validate`: Recursive checking ex: `Validate()` 237 | - `In`: Rule to constrain a type to be `in` a collection 238 | ex: `In(["apple", "banana", "orange", "pear"], "Value need to be a fruit")` 239 | - `All`: Rule to constrain a collection to valid the specified rule 240 | ex: `All(MinLength(1), "You can't use empty string")` 241 | - `RegEx`: check if a type that implement `AsRef` (String, &str, ...) matches the regex. 242 | You need the `regex` feature to use it. 243 | ex: `RegEx(r"^\S+@\S+\.\S+")` 244 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "type-rules-derive" 3 | version = "0.2.3" 4 | edition = "2021" 5 | readme = "../README.md" 6 | description = "type-rules derive macro" 7 | homepage = "https://github.com/TheoOiry/type-rules" 8 | repository = "https://github.com/TheoOiry/type-rules" 9 | documentation = "https://docs.rs/type-rules" 10 | keywords = ["rule", "type", "contraint", "restrict", "validate"] 11 | categories = ["data-structures"] 12 | license = "MIT OR Apache-2.0" 13 | authors = ["Théo Oiry "] 14 | 15 | [dependencies] 16 | proc-macro2 = "1.0" 17 | syn = { version = "1.0", features = ["parsing", "extra-traits", "full"] } 18 | quote = "1.0" 19 | 20 | [lib] 21 | proc-macro = true 22 | 23 | 24 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | extern crate proc_macro2; 3 | 4 | use crate::parsing::from_ast; 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | use syn::{parse_macro_input, DeriveInput}; 8 | 9 | mod parsing; 10 | 11 | #[proc_macro_derive(Validator, attributes(rule))] 12 | pub fn derive_validator(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 13 | let input = parse_macro_input!(input as DeriveInput); 14 | 15 | expand_derive_validator(input).into() 16 | } 17 | 18 | fn expand_derive_validator(input: DeriveInput) -> TokenStream { 19 | let name = input.ident; 20 | let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); 21 | let body = from_ast(&input.data).body(&name); 22 | 23 | quote! { 24 | impl #impl_generics type_rules::Validator for #name #type_generics #where_clause { 25 | fn check_validity(&self) -> Result<(), String> { 26 | #body 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /derive/src/parsing.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::punctuated::Punctuated; 5 | use syn::Token; 6 | 7 | pub enum Data { 8 | Struct(Struct), 9 | Enum(Vec), 10 | } 11 | 12 | pub struct Struct { 13 | fields: Vec, 14 | style: Style, 15 | } 16 | 17 | pub struct Field { 18 | member: syn::Member, 19 | rules: Vec, 20 | } 21 | 22 | pub enum Style { 23 | Struct, 24 | Tuple, 25 | Unit, 26 | } 27 | 28 | pub struct Variant { 29 | ident: syn::Ident, 30 | def: Struct, 31 | } 32 | 33 | struct RuleAttribute { 34 | expr: Punctuated, 35 | } 36 | 37 | pub fn from_ast(data: &syn::Data) -> Data { 38 | match data { 39 | syn::Data::Enum(data) => Data::Enum(enum_from_ast(&data.variants)), 40 | syn::Data::Struct(data) => Data::Struct(struct_from_ast(&data.fields)), 41 | syn::Data::Union(_) => panic!("Validator derive is not implemented for Union"), 42 | } 43 | } 44 | 45 | fn enum_from_ast(variants: &Punctuated) -> Vec { 46 | variants 47 | .iter() 48 | .map(|var| Variant { 49 | ident: var.ident.clone(), 50 | def: struct_from_ast(&var.fields), 51 | }) 52 | .collect() 53 | } 54 | 55 | fn struct_from_ast(fields: &syn::Fields) -> Struct { 56 | match fields { 57 | syn::Fields::Named(fields) => Struct { 58 | fields: fields_from_ast(&fields.named), 59 | style: Style::Struct, 60 | }, 61 | syn::Fields::Unnamed(fields) => Struct { 62 | fields: fields_from_ast(&fields.unnamed), 63 | style: Style::Tuple, 64 | }, 65 | syn::Fields::Unit => Struct { 66 | fields: Vec::new(), 67 | style: Style::Unit, 68 | }, 69 | } 70 | } 71 | 72 | fn fields_from_ast(fields: &Punctuated) -> Vec { 73 | fields 74 | .iter() 75 | .enumerate() 76 | .map(|(i, field)| Field { 77 | member: match &field.ident { 78 | Some(ident) => syn::Member::Named(ident.clone()), 79 | None => syn::Member::Unnamed(i.into()), 80 | }, 81 | rules: RuleAttribute::parse_attributes(&field.attrs), 82 | }) 83 | .collect() 84 | } 85 | 86 | impl Parse for RuleAttribute { 87 | fn parse(input: ParseStream) -> syn::Result { 88 | Ok(Self { 89 | expr: input.parse_terminated(syn::Expr::parse)?, 90 | }) 91 | } 92 | } 93 | 94 | impl RuleAttribute { 95 | pub fn parse_attributes(attrs: &[syn::Attribute]) -> Vec { 96 | attrs 97 | .iter() 98 | .filter(|attr| attr.path.is_ident("rule")) 99 | .flat_map(Self::parse_attribute) 100 | .collect() 101 | } 102 | 103 | fn parse_attribute(attr: &syn::Attribute) -> Vec { 104 | attr.parse_args::() 105 | .unwrap() 106 | .expr 107 | .into_iter() 108 | .collect() 109 | } 110 | } 111 | 112 | impl Data { 113 | pub fn body(&self, ident: &syn::Ident) -> TokenStream { 114 | match self { 115 | Data::Enum(variants) => Self::enum_body(variants, ident), 116 | Data::Struct(data) => Self::struct_body(data), 117 | } 118 | } 119 | 120 | fn enum_body(variants: &[Variant], ident: &syn::Ident) -> TokenStream { 121 | let variants_arms = variants.iter().map(|variant| variant.match_arm(ident)); 122 | quote! { 123 | match self { 124 | #( #variants_arms ),* 125 | } 126 | } 127 | } 128 | 129 | fn struct_body(data: &Struct) -> TokenStream { 130 | let fields_rules = data.fields.iter().map(Field::rules); 131 | quote! { 132 | #( #fields_rules )* 133 | Ok(()) 134 | } 135 | } 136 | } 137 | 138 | impl Variant { 139 | fn match_arm(&self, data_ident: &syn::Ident) -> TokenStream { 140 | let case = self.match_arm_case(data_ident); 141 | let fields_rules = self.def.fields.iter().map(Field::rules_named); 142 | quote! { 143 | #case => { 144 | #( #fields_rules )* 145 | Ok(()) 146 | } 147 | } 148 | } 149 | 150 | fn match_arm_case(&self, data_ident: &syn::Ident) -> TokenStream { 151 | let ident = &self.ident; 152 | let fields_names = self.def.fields.iter().map(Field::get_named_ident); 153 | match self.def.style { 154 | Style::Struct => quote! { 155 | #data_ident::#ident{#( #fields_names ),*} 156 | }, 157 | Style::Tuple => quote! { 158 | #data_ident::#ident(#( #fields_names ),*) 159 | }, 160 | Style::Unit => quote! { 161 | #data_ident::#ident 162 | }, 163 | } 164 | } 165 | } 166 | 167 | impl Field { 168 | fn rules(&self) -> TokenStream { 169 | let Self { rules, member } = &self; 170 | quote! { 171 | #( type_rules::Rule::check(&#rules, &self.#member)?; )* 172 | } 173 | } 174 | 175 | fn rules_named(&self) -> TokenStream { 176 | let ident = self.get_named_ident(); 177 | let rules = &self.rules; 178 | quote! { 179 | #( type_rules::Rule::check(&#rules, #ident)?; )* 180 | } 181 | } 182 | 183 | fn get_named_ident(&self) -> syn::Ident { 184 | match &self.member { 185 | syn::Member::Named(ident) => ident.clone(), 186 | syn::Member::Unnamed(index) => format_ident!("__field{}", index), 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/derive.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use type_rules::prelude::*; 3 | 4 | #[derive(Validator)] 5 | struct Email( 6 | #[rule( 7 | MaxLength(200), 8 | RegEx( 9 | r"^([a-z0-9_+]([a-z0-9_+.]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})" 10 | ) 11 | )] 12 | String, 13 | ); 14 | 15 | #[allow(dead_code)] 16 | #[derive(Validator)] 17 | enum MyEnum { 18 | Option1(#[rule(MaxLength(200))] String), 19 | Option2 { 20 | #[rule(MinMaxRange(1, 10))] 21 | integer: u32, 22 | }, 23 | Option3, 24 | } 25 | 26 | #[derive(Validator)] 27 | struct SignUpForm { 28 | #[rule(Validate())] 29 | email: Email, 30 | #[rule(MinMaxLength(8, 50))] 31 | password: String, 32 | } 33 | 34 | #[derive(Validator)] 35 | struct PastDate(#[rule(MaxRange(Utc::now()))] DateTime); 36 | 37 | #[derive(Validator)] 38 | struct FloatWrapper(#[rule(MinMaxRange(0_f32, 100_f32))] f32); 39 | 40 | #[derive(Validator)] 41 | struct VecWrapper(#[rule(MinMaxSize(1, 50))] Vec); 42 | 43 | fn main() {} 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![doc = include_str!("../README.md")] 3 | 4 | /// A module that contains all the rules 5 | pub mod rules; 6 | 7 | mod valid; 8 | 9 | /// A convenience module appropriate for glob imports `use type_rules::prelude::*;` 10 | pub mod prelude; 11 | 12 | #[cfg(feature = "derive")] 13 | #[doc(hidden)] 14 | pub use type_rules_derive::*; 15 | 16 | #[doc(inline)] 17 | pub use rules::Rule; 18 | 19 | #[doc(inline)] 20 | pub use valid::Valid; 21 | 22 | /// Check the validity of a type 23 | /// 24 | /// By implementing `Validator` for a type, you define the 25 | /// rules to check is validity 26 | /// 27 | /// Can be derived with the `derive` feature 28 | /// 29 | /// # Example 30 | /// 31 | /// Basic usage: 32 | /// 33 | /// ```should_panic 34 | /// use type_rules::prelude::*; 35 | /// 36 | /// #[derive(Validator)] 37 | /// struct NotEmptyString(#[rule(MinLength(1))] String); 38 | /// 39 | /// let valid = NotEmptyString(String::from("Not empty")); 40 | /// let not_valid = NotEmptyString(String::from("")); 41 | /// 42 | /// valid.check_validity().unwrap(); // OK 43 | /// not_valid.check_validity().unwrap(); // Value is too short 44 | /// ``` 45 | pub trait Validator { 46 | fn check_validity(&self) -> Result<(), String>; 47 | } 48 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use super::rules::*; 2 | pub use super::{Rule, Valid, Validator}; 3 | -------------------------------------------------------------------------------- /src/rules/all.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to constrain an iterable collection (with reference) 4 | /// to valid the specified rule and an error message 5 | /// 6 | /// # Example 7 | /// ``` 8 | /// use type_rules::prelude::*; 9 | /// 10 | /// #[derive(Validator)] 11 | /// struct VecNotEmptyString( 12 | /// #[rule(All(MinLength(1), "You can't use empty string"))] 13 | /// Vec 14 | /// ); 15 | /// ``` 16 | pub struct All<'a, T>(pub T, pub &'a str); 17 | 18 | impl<'a, T, U> Rule for All<'a, T> 19 | where 20 | U: IntoIterator, 21 | for<'b> &'b U: IntoIterator, 22 | T: Rule<::Item>, 23 | { 24 | fn check(&self, value: &U) -> Result<(), String> { 25 | match value 26 | .into_iter() 27 | .all(|v: &::Item| self.0.check(v).is_ok()) 28 | { 29 | true => Ok(()), 30 | false => Err(String::from(self.1)), 31 | } 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crate::rules::{All, MinRange, Rule}; 38 | use claim::{assert_err, assert_ok}; 39 | 40 | const ERROR_MESSAGE: &str = "Value need to be >= 1"; 41 | const RULE: All> = All(MinRange(1), ERROR_MESSAGE); 42 | 43 | #[test] 44 | fn all_ok() { 45 | assert_ok!(RULE.check(&vec![1, 1])); 46 | } 47 | #[test] 48 | fn all_err() { 49 | assert_err!(RULE.check(&vec![1, 0])); 50 | } 51 | #[test] 52 | fn all_good_error_message() { 53 | let res_error_message = RULE.check(&vec![1, 0]).expect_err("Should be an Err"); 54 | assert_eq!(res_error_message, ERROR_MESSAGE); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/rules/and.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | #[cfg(doc)] 4 | use super::{All, Opt}; 5 | 6 | /// Rule to ensure that 2 other rules are [`Ok`]. 7 | /// 8 | /// The `rule` attribute accepts multiple rules and has the same behavior 9 | /// but the [`And`] rule is useful for rules that accept a single rule such 10 | /// as [`All`] and [`Opt`]. 11 | /// 12 | /// In case of error on both rules, the first one is returned. 13 | /// 14 | /// # Example 15 | /// ``` 16 | /// use type_rules::prelude::*; 17 | /// 18 | /// #[derive(Validator)] 19 | /// struct OptionalMail( 20 | /// #[rule(Opt(And(MaxLength(1000), RegEx(r"^\S+@\S+\.\S+"))))] 21 | /// Option 22 | /// ); 23 | /// ``` 24 | pub struct And(pub T, pub U); 25 | 26 | impl Rule for And 27 | where 28 | T: Rule, 29 | U: Rule, 30 | { 31 | fn check(&self, value: &F) -> Result<(), String> { 32 | self.0.check(value)?; 33 | self.1.check(value) 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use crate::prelude::*; 40 | use claim::assert_ok; 41 | 42 | const RULE: Opt> = Opt(And(MaxLength(20), RegEx(r"^\S+@\S+\.\S+"))); 43 | 44 | #[test] 45 | fn and_ok() { 46 | assert_ok!(RULE.check(&Some("example@example.fr"))); 47 | } 48 | 49 | #[test] 50 | fn and_0_err() { 51 | let val = Some("too.long.example@too.long.example.fr"); 52 | 53 | let res_error_message = RULE.check(&val).expect_err("Should be an Err"); 54 | 55 | assert_eq!(res_error_message, "Value is too long") 56 | } 57 | 58 | #[test] 59 | fn and_1_err() { 60 | let val = Some("example.example.fr"); 61 | 62 | let res_error_message = RULE.check(&val).expect_err("Should be an Err"); 63 | 64 | assert_eq!(res_error_message, "The regex does not match") 65 | } 66 | 67 | #[test] 68 | fn and_0_1_err() { 69 | let val = Some("too.long.example.too.long.example.fr"); 70 | 71 | let res_error_message = RULE.check(&val).expect_err("Should be an Err"); 72 | 73 | assert_eq!(res_error_message, "Value is too long") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/rules/any.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to constrain an iterable collection (with reference) 4 | /// to valid that any element of the collection valid the specified rule 5 | /// 6 | /// # Example 7 | /// ``` 8 | /// use type_rules::prelude::*; 9 | /// 10 | /// #[derive(Validator)] 11 | /// struct VecNotEmptyString( 12 | /// #[rule(Any(MinLength(10), "At least one element need to be of length >= 10"))] 13 | /// Vec 14 | /// ); 15 | /// ``` 16 | pub struct Any<'a, T>(pub T, pub &'a str); 17 | 18 | impl<'a, T, U> Rule for Any<'a, T> 19 | where 20 | U: IntoIterator, 21 | for<'b> &'b U: IntoIterator, 22 | T: Rule<::Item>, 23 | { 24 | fn check(&self, value: &U) -> Result<(), String> { 25 | match value 26 | .into_iter() 27 | .any(|v: &::Item| self.0.check(v).is_ok()) 28 | { 29 | true => Ok(()), 30 | false => Err(String::from(self.1)), 31 | } 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crate::prelude::*; 38 | use claim::{assert_err, assert_ok}; 39 | 40 | const ERROR_MESSAGE: &str = "At least one element need to be >= 1"; 41 | const RULE: Any> = Any(MinRange(1), ERROR_MESSAGE); 42 | 43 | #[test] 44 | fn any_ok() { 45 | assert_ok!(RULE.check(&vec![0, 1])); 46 | } 47 | #[test] 48 | fn any_err() { 49 | assert_err!(RULE.check(&vec![0, 0])); 50 | } 51 | #[test] 52 | fn any_good_error_message() { 53 | let res_error_message = RULE.check(&vec![0, 0]).expect_err("Should be an Err"); 54 | assert_eq!(res_error_message, ERROR_MESSAGE); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/rules/eval.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to constrain any type to a predicate 4 | /// 5 | /// The value is always passed by its reference 6 | /// 7 | /// If you pass a closure make sure that the value 8 | /// are typed, without this, rust consider that your 9 | /// closure is not general enough because it can't ensure 10 | /// that it implements FnOnce for any lifetime 11 | /// 12 | /// # Example 13 | /// ``` 14 | /// use type_rules::prelude::*; 15 | /// 16 | /// #[derive(Validator)] 17 | /// struct EvenInteger( 18 | /// #[rule(Eval(|v: &u32| v % 2 == 0, "Value need to be even"))] 19 | /// u32 20 | /// ); 21 | /// ``` 22 | pub struct Eval<'a, T>(pub T, pub &'a str); 23 | 24 | impl<'a, T, U> Rule for Eval<'a, T> 25 | where 26 | T: Fn(&U) -> bool, 27 | { 28 | fn check(&self, value: &U) -> Result<(), String> { 29 | match self.0(value) { 30 | true => Ok(()), 31 | false => Err(String::from(self.1)), 32 | } 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use crate::rules::{Eval, Rule}; 39 | use claim::{assert_err, assert_ok}; 40 | 41 | const ERROR_MESSAGE: &str = "Value need to be even"; 42 | const RULE: Eval fn(&'a u32) -> bool> = Eval(|v| v % 2 == 0, ERROR_MESSAGE); 43 | 44 | #[test] 45 | fn eval_ok() { 46 | assert_ok!(RULE.check(&2)); 47 | } 48 | #[test] 49 | fn eval_err() { 50 | assert_err!(RULE.check(&1)); 51 | } 52 | #[test] 53 | fn eval_good_error_message() { 54 | let res_error_message = RULE.check(&1).expect_err("Should be an Err"); 55 | assert_eq!(res_error_message, ERROR_MESSAGE); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/rules/is_in.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to constrain a type to be `in` a collection 4 | /// 5 | /// # Example 6 | /// ``` 7 | /// use type_rules::prelude::*; 8 | /// 9 | /// #[derive(Validator)] 10 | /// struct Fruit(#[rule(In(["apple", "banana", "orange", "pear"], "Value need to be a fruit"))] String); 11 | /// ``` 12 | pub struct In<'a, T>(pub T, pub &'a str); 13 | 14 | impl<'a, T, U> Rule for In<'a, T> 15 | where 16 | T: IntoIterator, 17 | for<'b> &'b T: IntoIterator, 18 | U: PartialEq, 19 | { 20 | fn check(&self, value: &U) -> Result<(), String> { 21 | if (&self.0).into_iter().any(|v| value == v) { 22 | Ok(()) 23 | } else { 24 | Err(String::from(self.1)) 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use crate::prelude::*; 32 | use claim::assert_ok; 33 | 34 | const ERROR_MESSAGE: &str = "Value need to be a fruit"; 35 | const RULE: In<[&str; 4]> = In(["apple", "banana", "orange", "pear"], ERROR_MESSAGE); 36 | 37 | #[test] 38 | fn in_ok() { 39 | assert_ok!(RULE.check(&"banana")); 40 | } 41 | #[test] 42 | fn in_err() { 43 | let res_error_message = RULE.check(&"sandwich").expect_err("Should be an Err"); 44 | assert_eq!(res_error_message, ERROR_MESSAGE); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/rules/min_max_length.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to constraint the **minimum** and **maximum** 4 | /// length of any type that implements [`AsRef`] such 5 | /// as [`String`] or `&str` 6 | /// 7 | /// # Example 8 | /// ``` 9 | /// use type_rules::prelude::*; 10 | /// 11 | /// #[derive(Validator)] 12 | /// struct NewUser { 13 | /// #[rule(MinMaxLength(3, 50))] 14 | /// username: String, 15 | /// #[rule(MinMaxLength(8, 100))] 16 | /// password: String, 17 | /// } 18 | /// ``` 19 | pub struct MinMaxLength(pub usize, pub usize); 20 | 21 | /// Rule to constraint the **minimum** 22 | /// length of any type that implements [`AsRef`] such 23 | /// as [`String`] or `&str` 24 | /// 25 | /// # Example 26 | /// ``` 27 | /// use type_rules::prelude::*; 28 | /// 29 | /// #[derive(Validator)] 30 | /// struct Password(#[rule(MinLength(8))] String); 31 | /// ``` 32 | pub struct MinLength(pub usize); 33 | 34 | /// Rule to constraint the **maximum** 35 | /// length of any type that implements [`AsRef`] such 36 | /// as [`String`] or `&str` 37 | /// 38 | /// # Example 39 | /// ``` 40 | /// use type_rules::prelude::*; 41 | /// 42 | /// #[derive(Validator)] 43 | /// struct Payload(#[rule(MaxLength(200))] String); 44 | /// ``` 45 | pub struct MaxLength(pub usize); 46 | 47 | impl + ?Sized> Rule for MinMaxLength { 48 | fn check(&self, value: &T) -> Result<(), String> { 49 | let value = value.as_ref(); 50 | check_value_too_short(value.len(), self.0)?; 51 | check_value_too_long(value.len(), self.1) 52 | } 53 | } 54 | 55 | impl + ?Sized> Rule for MaxLength { 56 | fn check(&self, value: &T) -> Result<(), String> { 57 | let value = value.as_ref(); 58 | check_value_too_long(value.len(), self.0) 59 | } 60 | } 61 | 62 | impl + ?Sized> Rule for MinLength { 63 | fn check(&self, value: &T) -> Result<(), String> { 64 | let value = value.as_ref(); 65 | check_value_too_short(value.len(), self.0) 66 | } 67 | } 68 | 69 | fn check_value_too_short(length: usize, min_length: usize) -> Result<(), String> { 70 | if min_length > length { 71 | return Err(String::from("Value is too short")); 72 | } 73 | Ok(()) 74 | } 75 | 76 | fn check_value_too_long(length: usize, max_length: usize) -> Result<(), String> { 77 | if max_length < length { 78 | return Err(String::from("Value is too long")); 79 | } 80 | Ok(()) 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use crate::rules::{MaxLength, MinLength, MinMaxLength, Rule}; 86 | use claim::{assert_err, assert_ok}; 87 | 88 | #[test] 89 | fn min_length_value_too_short() { 90 | assert_err!(MinLength(1).check("")); 91 | } 92 | #[test] 93 | fn min_length_value_ok() { 94 | assert_ok!(MinLength(1).check("a")); 95 | } 96 | #[test] 97 | fn max_length_value_too_long() { 98 | assert_err!(MaxLength(1).check("aa")); 99 | } 100 | #[test] 101 | fn max_length_value_ok() { 102 | assert_ok!(MaxLength(1).check("a")); 103 | } 104 | #[test] 105 | fn min_max_length_value_too_short() { 106 | assert_err!(MinMaxLength(1, 10).check("")); 107 | } 108 | #[test] 109 | fn min_max_length_value_too_long() { 110 | assert_err!(MinMaxLength(0, 1).check("aa")); 111 | } 112 | #[test] 113 | fn min_max_length_value_ok() { 114 | assert_ok!(MinMaxLength(0, 1).check("a")); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/rules/min_max_range.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to constraint the **minimum** and **maximum** 4 | /// range of any type that implement [`PartialOrd`] 5 | /// 6 | /// # Example 7 | /// ``` 8 | /// use type_rules::prelude::*; 9 | /// 10 | /// #[derive(Validator)] 11 | /// struct QueryParameters{ 12 | /// offset: u32, 13 | /// #[rule(MinMaxRange(5, 50))] 14 | /// limit: u8, 15 | /// } 16 | /// ``` 17 | pub struct MinMaxRange>(pub T, pub T); 18 | 19 | /// Rule to constraint the **minimum** 20 | /// range of any type that implement [`PartialOrd`] 21 | /// 22 | /// # Example 23 | /// ``` 24 | /// use type_rules::prelude::*; 25 | /// 26 | /// #[derive(Validator)] 27 | /// struct NonZeroFloat(#[rule(MinRange(1_f32))] f32); 28 | /// ``` 29 | pub struct MinRange>(pub T); 30 | 31 | /// Rule to constraint the **maximum** 32 | /// range of any type that implement [`PartialOrd`] 33 | /// 34 | /// # Example 35 | /// ``` 36 | /// use type_rules::prelude::*; 37 | /// use chrono::prelude::*; 38 | /// 39 | /// #[derive(Validator)] 40 | /// struct BirthDate(#[rule(MaxRange(Utc::now()))] DateTime); 41 | /// ``` 42 | pub struct MaxRange>(pub T); 43 | 44 | impl> Rule for MinMaxRange { 45 | fn check(&self, value: &T) -> Result<(), String> { 46 | check_value_too_low(value, &self.0)?; 47 | check_value_too_high(value, &self.1)?; 48 | Ok(()) 49 | } 50 | } 51 | 52 | impl> Rule for MinRange { 53 | fn check(&self, value: &T) -> Result<(), String> { 54 | check_value_too_low(value, &self.0)?; 55 | Ok(()) 56 | } 57 | } 58 | 59 | impl> Rule for MaxRange { 60 | fn check(&self, value: &T) -> Result<(), String> { 61 | check_value_too_high(value, &self.0)?; 62 | Ok(()) 63 | } 64 | } 65 | 66 | fn check_value_too_low>(value: &T, min_range: &T) -> Result<(), String> { 67 | if value < min_range { 68 | return Err(String::from("Value is too low")); 69 | } 70 | Ok(()) 71 | } 72 | 73 | fn check_value_too_high>(value: &T, max_range: &T) -> Result<(), String> { 74 | if value > max_range { 75 | return Err(String::from("Value is too high")); 76 | } 77 | Ok(()) 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use crate::rules::{MaxRange, MinMaxRange, MinRange, Rule}; 83 | use claim::{assert_err, assert_ok}; 84 | 85 | #[test] 86 | fn min_range_value_too_short() { 87 | assert_err!(MinRange(10).check(&9)); 88 | } 89 | #[test] 90 | fn min_range_value_ok() { 91 | assert_ok!(MinRange(10).check(&10)); 92 | } 93 | #[test] 94 | fn max_range_value_too_long() { 95 | assert_err!(MaxRange(10).check(&11)); 96 | } 97 | #[test] 98 | fn max_range_value_ok() { 99 | assert_ok!(MaxRange(10).check(&10)); 100 | } 101 | #[test] 102 | fn min_max_range_value_too_short() { 103 | assert_err!(MinMaxRange(10, 100).check(&5)); 104 | } 105 | #[test] 106 | fn min_max_range_value_too_long() { 107 | assert_err!(MinMaxRange(10, 100).check(&101)); 108 | } 109 | #[test] 110 | fn min_max_range_value_ok() { 111 | assert_ok!(MinMaxRange(10, 100).check(&50)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/rules/min_max_size.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to constraint the **minimum** and **maximum** 4 | /// size of a [`Vec`] 5 | /// 6 | /// # Example 7 | /// ``` 8 | /// use type_rules::prelude::*; 9 | /// 10 | /// #[derive(Validator)] 11 | /// struct Car { 12 | /// #[rule(MinMaxSize(4, 4))] 13 | /// wheels_diameter: Vec 14 | /// }; 15 | /// ``` 16 | pub struct MinMaxSize(pub usize, pub usize); 17 | 18 | /// Rule to constraint the **minimum** 19 | /// size of a [`Vec`] 20 | /// 21 | /// # Example 22 | /// ``` 23 | /// use type_rules::prelude::*; 24 | /// 25 | /// #[derive(Validator)] 26 | /// struct NonEmptyVec(#[rule(MinSize(1))] Vec); 27 | /// ``` 28 | pub struct MinSize(pub usize); 29 | 30 | /// Rule to constraint the **maximum** 31 | /// size of a [`Vec`] 32 | /// 33 | /// # Example 34 | /// ``` 35 | /// use type_rules::prelude::*; 36 | /// 37 | /// #[derive(Validator)] 38 | /// struct FollowedCategories(#[rule(MaxSize(100))] Vec); 39 | /// ``` 40 | pub struct MaxSize(pub usize); 41 | 42 | impl Rule> for MinMaxSize { 43 | fn check(&self, value: &Vec) -> Result<(), String> { 44 | let size = value.len(); 45 | check_value_too_short(size, self.0)?; 46 | check_value_too_long(size, self.1)?; 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl Rule> for MinSize { 52 | fn check(&self, value: &Vec) -> Result<(), String> { 53 | check_value_too_short(value.len(), self.0)?; 54 | Ok(()) 55 | } 56 | } 57 | 58 | impl Rule> for MaxSize { 59 | fn check(&self, value: &Vec) -> Result<(), String> { 60 | check_value_too_long(value.len(), self.0)?; 61 | Ok(()) 62 | } 63 | } 64 | 65 | fn check_value_too_short(length: usize, min_size: usize) -> Result<(), String> { 66 | if length < min_size { 67 | return Err(String::from("Collection is too short")); 68 | } 69 | Ok(()) 70 | } 71 | 72 | fn check_value_too_long(length: usize, max_size: usize) -> Result<(), String> { 73 | if length > max_size { 74 | return Err(String::from("Collection is too long")); 75 | } 76 | Ok(()) 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use crate::rules::{MaxSize, MinMaxSize, MinSize, Rule}; 82 | use claim::{assert_err, assert_ok}; 83 | 84 | #[test] 85 | fn min_size_value_too_short() { 86 | assert_err!(MinSize(1).check(&Vec::::new())); 87 | } 88 | #[test] 89 | fn min_size_value_ok() { 90 | assert_ok!(MinSize(1).check(&vec![1, 2])); 91 | } 92 | #[test] 93 | fn max_size_value_too_long() { 94 | assert_err!(MaxSize(1).check(&vec![1, 2])); 95 | } 96 | #[test] 97 | fn max_size_value_ok() { 98 | assert_ok!(MaxSize(1).check(&vec![1])); 99 | } 100 | #[test] 101 | fn min_max_size_value_too_short() { 102 | assert_err!(MinMaxSize(1, 2).check(&Vec::::new())); 103 | } 104 | #[test] 105 | fn min_max_size_value_too_long() { 106 | assert_err!(MinMaxSize(1, 2).check(&vec![1, 2, 3])); 107 | } 108 | #[test] 109 | fn min_max_size_value_ok() { 110 | assert_ok!(MinMaxSize(1, 2).check(&vec![1])); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/rules/mod.rs: -------------------------------------------------------------------------------- 1 | mod all; 2 | mod and; 3 | mod any; 4 | mod eval; 5 | mod is_in; 6 | mod min_max_length; 7 | mod min_max_range; 8 | mod min_max_size; 9 | mod opt; 10 | mod or; 11 | #[cfg(feature = "regex")] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "regex")))] 13 | mod regex; 14 | mod validate; 15 | 16 | pub use self::all::*; 17 | pub use self::and::*; 18 | pub use self::any::*; 19 | pub use self::eval::*; 20 | pub use self::is_in::*; 21 | pub use self::min_max_length::*; 22 | pub use self::min_max_range::*; 23 | pub use self::min_max_size::*; 24 | pub use self::opt::*; 25 | pub use self::or::*; 26 | pub use self::validate::*; 27 | 28 | #[cfg(feature = "regex")] 29 | #[cfg_attr(docsrs, doc(cfg(feature = "regex")))] 30 | pub use self::regex::*; 31 | 32 | /// Define a rule for a type 33 | /// 34 | /// By implementing `Rule` for a type you define how 35 | /// it will be used to constraint a type `T` 36 | /// 37 | /// # Example 38 | /// 39 | /// ``` 40 | /// use type_rules::prelude::*; 41 | /// 42 | /// struct IsEven(); 43 | /// 44 | /// impl Rule for IsEven { 45 | /// fn check(&self, value: &i32) -> Result<(), String> { 46 | /// if value % 2 == 0 { 47 | /// Ok(()) 48 | /// } else { 49 | /// Err("Value is not even".into()) 50 | /// } 51 | /// } 52 | /// } 53 | /// 54 | /// #[derive(Validator)] 55 | /// struct MyInteger(#[rule(IsEven())] i32); 56 | /// ``` 57 | pub trait Rule { 58 | fn check(&self, value: &T) -> Result<(), String>; 59 | } 60 | -------------------------------------------------------------------------------- /src/rules/opt.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to apply another rule to inner value of an [`Option`] 4 | /// 5 | /// In case of a [`None`] value, it just return `Ok(())` 6 | /// 7 | /// # Example 8 | /// ``` 9 | /// use type_rules::prelude::*; 10 | /// 11 | /// #[derive(Validator)] 12 | /// struct Parameters { 13 | /// #[rule(Opt(MinMaxRange(1, 4)))] 14 | /// optional_parameter: Option, 15 | /// } 16 | /// 17 | /// let param = Parameters { optional_parameter: Some(1) }; 18 | /// assert!(param.check_validity().is_ok()); 19 | /// 20 | /// let param = Parameters { optional_parameter: None }; 21 | /// assert!(param.check_validity().is_ok()); 22 | /// 23 | /// let param = Parameters { optional_parameter: Some(5) }; 24 | /// assert!(param.check_validity().is_err()); 25 | /// ``` 26 | pub struct Opt(pub T); 27 | 28 | impl> Rule> for Opt { 29 | fn check(&self, value: &Option) -> Result<(), String> { 30 | match value { 31 | Some(val) => self.0.check(val), 32 | None => Ok(()), 33 | } 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use crate::rules::{MinRange, Opt, Rule}; 40 | use claim::{assert_err, assert_ok}; 41 | 42 | const RULE: Opt> = Opt(MinRange(1)); 43 | 44 | #[test] 45 | fn opt_ok_with_some() { 46 | assert_ok!(RULE.check(&Some(1))); 47 | } 48 | #[test] 49 | fn opt_ok_with_none() { 50 | assert_ok!(RULE.check(&None)); 51 | } 52 | #[test] 53 | fn opt_err() { 54 | assert_err!(RULE.check(&Some(0))); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/rules/or.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | 3 | /// Rule to apply an Or condition on two rules. 4 | /// 5 | /// In case of error on both rules, the first one is returned. 6 | /// 7 | /// # Example 8 | /// ``` 9 | /// use type_rules::prelude::*; 10 | /// 11 | /// #[derive(Validator)] 12 | /// struct NotZeroInteger( 13 | /// #[rule(Or(MaxRange(-1), MinRange(1)))] 14 | /// i32 15 | /// ); 16 | /// ``` 17 | pub struct Or(pub T, pub U); 18 | 19 | impl Rule for Or 20 | where 21 | T: Rule, 22 | U: Rule, 23 | { 24 | fn check(&self, value: &F) -> Result<(), String> { 25 | let first_res = self.0.check(value); 26 | let second_res = self.1.check(value); 27 | 28 | if first_res.is_ok() || second_res.is_ok() { 29 | Ok(()) 30 | } else { 31 | first_res 32 | } 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use crate::prelude::*; 39 | use claim::assert_ok; 40 | 41 | const RULE: Or, MinRange> = Or(MaxRange(-1), MinRange(1)); 42 | 43 | #[test] 44 | fn or_0_ok() { 45 | assert_ok!(RULE.check(&-1)); 46 | } 47 | 48 | #[test] 49 | fn or_1_ok() { 50 | assert_ok!(RULE.check(&1)); 51 | } 52 | 53 | #[test] 54 | fn or_err() { 55 | let res_error_message = RULE.check(&0).expect_err("Should be an Err"); 56 | 57 | assert_eq!(res_error_message, "Value is too high") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/rules/regex.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | use regex::Regex; 3 | 4 | /// Rule to constraint any type that implements [`AsRef`] such 5 | /// as [`String`] or `&str` to match a Regex 6 | /// 7 | /// You need the `regex` feature to use it 8 | /// 9 | /// # Example 10 | /// ``` 11 | /// use type_rules::prelude::*; 12 | /// 13 | /// #[derive(Validator)] 14 | /// struct Mail(#[rule(RegEx(r"^\S+@\S+\.\S+"))] String); 15 | /// ``` 16 | pub struct RegEx<'a>(pub &'a str); 17 | 18 | impl<'a, T: AsRef + ?Sized> Rule for RegEx<'a> { 19 | fn check(&self, value: &T) -> Result<(), String> { 20 | check(self.0, value.as_ref()) 21 | } 22 | } 23 | 24 | fn check(regex: &str, value: &str) -> Result<(), String> { 25 | let regex = Regex::new(regex).expect("Invalid Regex"); 26 | if regex.is_match(value) { 27 | return Ok(()); 28 | } 29 | Err(String::from("The regex does not match")) 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use crate::rules::{RegEx, Rule}; 35 | use claim::{assert_err, assert_ok}; 36 | 37 | #[test] 38 | fn regex_ok() { 39 | assert_ok!(RegEx(r"^\S+@\S+\.\S+").check("example@example.fr")); 40 | } 41 | #[test] 42 | fn regex_err() { 43 | assert_err!(RegEx(r"^\S+@\S+\.\S+").check("exampleexample.fr")); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/rules/validate.rs: -------------------------------------------------------------------------------- 1 | use super::Rule; 2 | use crate::Validator; 3 | 4 | /// Rule to check the rules of the inner type 5 | /// 6 | /// # Example 7 | /// ``` 8 | /// use type_rules::prelude::*; 9 | /// 10 | /// #[derive(Validator)] 11 | /// struct Password(#[rule(MinLength(8))] String); 12 | /// 13 | /// #[derive(Validator)] 14 | /// struct User { 15 | /// username: String, 16 | /// #[rule(Validate())] 17 | /// password: Password, 18 | /// }; 19 | /// ``` 20 | pub struct Validate(); 21 | 22 | impl Rule for Validate { 23 | fn check(&self, value: &T) -> Result<(), String> { 24 | value.check_validity() 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use crate::rules::{MaxLength, Rule, Validate}; 31 | use crate::Validator; 32 | use claim::{assert_err, assert_ok}; 33 | 34 | struct StringWrapper(String); 35 | 36 | impl Validator for StringWrapper { 37 | fn check_validity(&self) -> Result<(), String> { 38 | MaxLength(2).check(&self.0) 39 | } 40 | } 41 | 42 | #[test] 43 | fn validate_ok() { 44 | assert_ok!(Validate().check(&StringWrapper(String::from("a")))); 45 | } 46 | #[test] 47 | fn validate_err() { 48 | assert_err!(Validate().check(&StringWrapper(String::from("aaa")))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/valid.rs: -------------------------------------------------------------------------------- 1 | use crate::Validator; 2 | use std::fmt; 3 | use std::ops::Deref; 4 | 5 | /// `Valid` is a wrapper for any type that implements [`Validator`] 6 | /// it permit to ensure at compile time that the inner type as been 7 | /// verified. 8 | /// 9 | /// With the [`serde`] feature, Valid can be serialized and deserialized 10 | /// with validity check. 11 | /// ``` 12 | /// use type_rules::prelude::*; 13 | /// 14 | /// #[derive(Validator)] 15 | /// struct NewUser { 16 | /// #[rule(MinMaxLength(3, 50))] 17 | /// username: String, 18 | /// #[rule(MinMaxLength(8, 100))] 19 | /// password: String, 20 | /// } 21 | /// 22 | /// fn do_something(user: Valid) { 23 | /// // No need to check if user is valid 24 | /// } 25 | /// 26 | /// let new_user = NewUser { 27 | /// username: "example".to_string(), 28 | /// password: "OPw$5%hJJ".to_string(), 29 | /// }; 30 | /// do_something(Valid::new(new_user).unwrap()); 31 | /// ``` 32 | #[derive(Debug)] 33 | pub struct Valid(T); 34 | 35 | impl Valid { 36 | #[allow(dead_code)] 37 | pub fn new(val: T) -> Result { 38 | val.check_validity()?; 39 | Ok(Valid(val)) 40 | } 41 | 42 | #[allow(dead_code)] 43 | pub fn into_inner(self) -> T { 44 | self.0 45 | } 46 | } 47 | 48 | impl Deref for Valid { 49 | type Target = T; 50 | 51 | fn deref(&self) -> &Self::Target { 52 | &self.0 53 | } 54 | } 55 | 56 | impl fmt::Display for Valid { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | fmt::Display::fmt(&self.0, f) 59 | } 60 | } 61 | 62 | #[cfg(feature = "serde")] 63 | #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 64 | impl<'a, T: Validator + serde::Deserialize<'a>> serde::Deserialize<'a> for Valid { 65 | fn deserialize(deserializer: D) -> Result 66 | where 67 | D: serde::Deserializer<'a>, 68 | { 69 | let v = T::deserialize(deserializer)?; 70 | Valid::new(v).map_err(serde::de::Error::custom) 71 | } 72 | } 73 | 74 | #[cfg(feature = "serde")] 75 | #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 76 | impl serde::Serialize for Valid { 77 | fn serialize(&self, serializer: S) -> Result 78 | where 79 | S: serde::Serializer, 80 | { 81 | self.0.serialize(serializer) 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use crate::valid::Valid; 88 | use crate::Validator; 89 | use claim::{assert_err, assert_ok}; 90 | 91 | #[derive(Debug)] 92 | struct ValidTest(i32); 93 | 94 | impl Validator for ValidTest { 95 | fn check_validity(&self) -> Result<(), String> { 96 | match self.0.is_positive() { 97 | true => Ok(()), 98 | false => Err(String::from("Need to be positive")), 99 | } 100 | } 101 | } 102 | 103 | #[test] 104 | fn valid_ok() { 105 | assert_ok!(Valid::new(ValidTest(1))); 106 | } 107 | 108 | #[test] 109 | fn valid_err() { 110 | assert_err!(Valid::new(ValidTest(-1))); 111 | } 112 | 113 | #[cfg(feature = "serde")] 114 | mod serde_tests { 115 | use crate::valid::Valid; 116 | use crate::Validator; 117 | use claim::{assert_err, assert_ok}; 118 | use serde_derive::{Deserialize, Serialize}; 119 | 120 | #[derive(Deserialize, Serialize, Debug)] 121 | struct Int { 122 | val: i32, 123 | } 124 | 125 | impl Validator for Int { 126 | fn check_validity(&self) -> Result<(), String> { 127 | match self.val.is_positive() { 128 | true => Ok(()), 129 | false => Err(String::from("Need to be positive")), 130 | } 131 | } 132 | } 133 | 134 | #[test] 135 | fn valid_serde_ok() { 136 | let serialized = "{\"val\":1}"; 137 | let res: serde_json::Result> = serde_json::from_str(serialized); 138 | assert_ok!(res); 139 | } 140 | 141 | #[test] 142 | fn valid_serde_err() { 143 | let serialized = "{\"val\":-1}"; 144 | let res: serde_json::Result> = serde_json::from_str(serialized); 145 | assert_err!(res); 146 | } 147 | 148 | #[test] 149 | fn valid_serde_serialize() { 150 | let valid_int = Valid::new(Int { val: 1 }).unwrap(); 151 | let serialized = serde_json::to_string(&valid_int).unwrap(); 152 | assert_eq!(&serialized, "{\"val\":1}") 153 | } 154 | } 155 | } 156 | --------------------------------------------------------------------------------