├── .gitignore ├── rustvalidity-derive ├── Cargo.toml └── src │ └── lib.rs ├── src ├── rules │ ├── mod.rs │ ├── numeric.rs │ ├── conditional.rs │ ├── advanced.rs │ ├── collection.rs │ └── common.rs ├── lib.rs ├── validator.rs └── error.rs ├── Cargo.toml ├── examples ├── derive_validation.rs ├── user_validation.rs └── attribute_validation.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /rustvalidity-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustvalidity-derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Derive macros for the rustvalidity crate" 6 | authors = ["Saeed Ghanbari"] 7 | license = "MIT" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | syn = { version = "2.0", features = ["full", "extra-traits"] } 14 | quote = "1.0" 15 | proc-macro2 = "1.0" 16 | -------------------------------------------------------------------------------- /src/rules/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ValidationError; 2 | 3 | pub mod common; 4 | pub mod numeric; 5 | pub mod collection; 6 | pub mod advanced; 7 | pub mod conditional; 8 | 9 | /// Trait that all validation rules must implement 10 | pub trait Rule: Send + Sync { 11 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError>; 12 | } 13 | 14 | /// Prelude module for commonly used rules 15 | pub mod prelude { 16 | pub use super::common::*; 17 | pub use super::numeric::*; 18 | pub use super::collection::*; 19 | pub use super::advanced::*; 20 | pub use super::conditional::*; 21 | } 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustvalidity" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "A powerful, flexible, and easy-to-use validation library for Rust" 6 | authors = ["Saeed Ghanbari"] 7 | license = "MIT" 8 | repository = "https://github.com/sgh370/rustvalidity" 9 | 10 | [dependencies] 11 | regex = "1.10.2" 12 | chrono = "0.4.31" 13 | serde = { version = "1.0.193", features = ["derive"] } 14 | serde_json = "1.0.108" 15 | thiserror = "1.0.50" 16 | url = "2.5.0" 17 | uuid = { version = "1.6.1", features = ["v4"] } 18 | rustvalidity-derive = { version = "0.1.0", path = "./rustvalidity-derive", optional = true } 19 | 20 | [features] 21 | default = [] 22 | derive = ["rustvalidity-derive"] 23 | 24 | [workspace] 25 | members = [ 26 | ".", 27 | "rustvalidity-derive", 28 | ] 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Rustvalidity 2 | //! 3 | //! Rustvalidity is a powerful, flexible, and easy-to-use validation library for Rust 4 | //! that provides struct-level validation through attributes. It offers a wide range 5 | //! of built-in validation rules and supports custom validation logic. 6 | //! 7 | //! # Author 8 | //! 9 | //! Rustvalidity was created by [Saeed Ghanbari](https://github.com/sgh370). 10 | 11 | pub mod rules; 12 | pub mod validator; 13 | pub mod error; 14 | 15 | pub use validator::Validator; 16 | pub use error::ValidationError; 17 | 18 | // Re-export the derive macro when the derive feature is enabled 19 | #[cfg(feature = "derive")] 20 | pub use rustvalidity_derive::Validate; 21 | 22 | /// Re-export commonly used items for easier imports 23 | pub mod prelude { 24 | pub use crate::validator::{Validator, Validate}; 25 | pub use crate::rules::Rule; 26 | pub use crate::error::ValidationError; 27 | pub use crate::rules::prelude::*; 28 | 29 | // Re-export the derive macro when the derive feature is enabled 30 | #[cfg(feature = "derive")] 31 | pub use rustvalidity_derive::Validate; 32 | } 33 | -------------------------------------------------------------------------------- /examples/derive_validation.rs: -------------------------------------------------------------------------------- 1 | use rustvalidity::prelude::*; 2 | 3 | // Define a struct with validation attributes 4 | #[derive(Debug, Validate)] 5 | struct Product { 6 | #[validate(required, length(min = 3, max = 50))] 7 | name: String, 8 | 9 | #[validate(min = 0)] 10 | price: f64, 11 | 12 | #[validate(required, email)] 13 | contact_email: String, 14 | 15 | #[validate(min = 1)] 16 | quantity: i32, 17 | 18 | #[validate(url)] 19 | website: String, 20 | } 21 | 22 | fn main() { 23 | // Create a valid product 24 | let valid_product = Product { 25 | name: "Awesome Product".to_string(), 26 | price: 29.99, 27 | contact_email: "sales@example.com".to_string(), 28 | quantity: 10, 29 | website: "https://example.com/products".to_string(), 30 | }; 31 | 32 | // Validate the product 33 | match valid_product.validate() { 34 | Ok(_) => println!("Valid product: {}", valid_product.name), 35 | Err(err) => println!("Validation failed: {}", err), 36 | } 37 | 38 | // Create an invalid product 39 | let invalid_product = Product { 40 | name: "Ab".to_string(), 41 | price: -10.0, 42 | contact_email: "not-an-email".to_string(), 43 | quantity: 0, 44 | website: "not-a-url".to_string(), 45 | }; 46 | 47 | // Validate the invalid product 48 | match invalid_product.validate() { 49 | Ok(_) => println!("Valid product: {}", invalid_product.name), 50 | Err(err) => println!("Validation failed: {}", err), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/validator.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::any::Any; 3 | use std::marker::PhantomData; 4 | 5 | use crate::error::ValidationError; 6 | use crate::rules::Rule; 7 | 8 | /// Trait for types that can be validated 9 | pub trait Validate { 10 | /// Validate the value and return a Result 11 | fn validate(&self) -> Result<(), ValidationError>; 12 | } 13 | 14 | /// Main validator struct that holds validation rules 15 | pub struct Validator { 16 | rules: HashMap>, 17 | } 18 | 19 | impl Validator { 20 | /// Create a new validator instance 21 | pub fn new() -> Self { 22 | Validator { 23 | rules: HashMap::new(), 24 | } 25 | } 26 | 27 | /// Add a rule to the validator 28 | pub fn add_rule(&mut self, name: &str, rule: R) 29 | where 30 | R: Rule + 'static, 31 | { 32 | self.rules.insert(name.to_string(), Box::new(rule)); 33 | } 34 | 35 | /// Get a rule by name 36 | pub fn get_rule(&self, name: &str) -> Option<&dyn Rule> { 37 | self.rules.get(name).map(|r| r.as_ref()) 38 | } 39 | 40 | /// Validate a value against the rules 41 | pub fn validate(&self, value: &T) -> Result<(), ValidationError> { 42 | // Use the Validate trait for validation 43 | value.validate() 44 | } 45 | 46 | /// Validate all fields and collect all errors 47 | pub fn validate_all(&self, value: &T) -> Result<(), ValidationError> { 48 | // Similar to validate, but collects all errors instead of stopping at the first one 49 | value.validate() 50 | } 51 | } 52 | 53 | impl Default for Validator { 54 | fn default() -> Self { 55 | Self::new() 56 | } 57 | } 58 | 59 | /// A pattern for combining multiple validation rules 60 | pub struct Pattern { 61 | rules: Vec>, 62 | _marker: PhantomData, 63 | } 64 | 65 | impl Pattern { 66 | /// Create a new pattern with the given rules 67 | pub fn new(rules: Vec>) -> Self { 68 | Pattern { 69 | rules, 70 | _marker: PhantomData, 71 | } 72 | } 73 | 74 | /// Validate a value against all rules in the pattern 75 | pub fn validate(&self, value: &T) -> Result<(), ValidationError> 76 | where 77 | T: Any, 78 | { 79 | for rule in &self.rules { 80 | if let Err(err) = rule.validate_any(value) { 81 | return Err(err); 82 | } 83 | } 84 | Ok(()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt; 3 | use thiserror::Error; 4 | 5 | /// Represents validation errors that can occur during validation 6 | #[derive(Debug, Clone)] 7 | pub enum ValidationError { 8 | /// A single validation error with a message 9 | Single(String), 10 | 11 | /// Multiple validation errors grouped by field 12 | Multiple(HashMap>), 13 | } 14 | 15 | impl ValidationError { 16 | /// Create a new single validation error 17 | pub fn new>(message: S) -> Self { 18 | ValidationError::Single(message.into()) 19 | } 20 | 21 | /// Create a new validation error for a specific field 22 | pub fn field, M: Into>(field: S, message: M) -> Self { 23 | let mut errors = HashMap::new(); 24 | errors.insert(field.into(), vec![message.into()]); 25 | ValidationError::Multiple(errors) 26 | } 27 | 28 | /// Merge multiple validation errors 29 | pub fn merge(self, other: ValidationError) -> ValidationError { 30 | match (self, other) { 31 | (ValidationError::Single(msg1), ValidationError::Single(msg2)) => { 32 | let mut errors = HashMap::new(); 33 | errors.insert("_".to_string(), vec![msg1, msg2]); 34 | ValidationError::Multiple(errors) 35 | }, 36 | (ValidationError::Single(msg), ValidationError::Multiple(mut errs)) => { 37 | let entry = errs.entry("_".to_string()).or_insert_with(Vec::new); 38 | entry.push(msg); 39 | ValidationError::Multiple(errs) 40 | }, 41 | (ValidationError::Multiple(mut errs), ValidationError::Single(msg)) => { 42 | let entry = errs.entry("_".to_string()).or_insert_with(Vec::new); 43 | entry.push(msg); 44 | ValidationError::Multiple(errs) 45 | }, 46 | (ValidationError::Multiple(mut errs1), ValidationError::Multiple(errs2)) => { 47 | for (field, messages) in errs2 { 48 | let entry = errs1.entry(field).or_insert_with(Vec::new); 49 | entry.extend(messages); 50 | } 51 | ValidationError::Multiple(errs1) 52 | } 53 | } 54 | } 55 | } 56 | 57 | impl fmt::Display for ValidationError { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | match self { 60 | ValidationError::Single(msg) => write!(f, "{}", msg), 61 | ValidationError::Multiple(errors) => { 62 | writeln!(f, "Validation errors:")?; 63 | for (field, messages) in errors { 64 | for msg in messages { 65 | writeln!(f, " {}: {}", field, msg)?; 66 | } 67 | } 68 | Ok(()) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/user_validation.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rustvalidity::prelude::*; 4 | use rustvalidity::error::ValidationError; 5 | use rustvalidity::validator::{Validator, Validate}; 6 | use rustvalidity::rules::{Rule, common, numeric, collection}; 7 | 8 | struct User { 9 | username: String, 10 | email: String, 11 | age: i32, 12 | interests: Vec, 13 | } 14 | 15 | impl Validate for User { 16 | fn validate(&self) -> Result<(), ValidationError> { 17 | // Create a new validator instance 18 | let mut validator = Validator::new(); 19 | 20 | // Add validation rules 21 | validator.add_rule("required", common::Required); 22 | validator.add_rule("username_length", common::Length { min: 3, max: Some(20) }); 23 | validator.add_rule("email", common::Email { check_dns: false }); 24 | validator.add_rule("min_age", numeric::Min { value: 18 }); 25 | validator.add_rule("interests_required", collection::MinSize { min: 1 }); 26 | 27 | // Validate individual fields 28 | let mut errors = HashMap::new(); 29 | 30 | // Validate username 31 | if let Err(err) = validator.get_rule("required") 32 | .unwrap() 33 | .validate_any(&self.username) { 34 | errors.entry("username".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 35 | } else if let Err(err) = validator.get_rule("username_length") 36 | .unwrap() 37 | .validate_any(&self.username) { 38 | errors.entry("username".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 39 | } 40 | 41 | // Validate email 42 | if let Err(err) = validator.get_rule("required") 43 | .unwrap() 44 | .validate_any(&self.email) { 45 | errors.entry("email".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 46 | } else if let Err(err) = validator.get_rule("email") 47 | .unwrap() 48 | .validate_any(&self.email) { 49 | errors.entry("email".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 50 | } 51 | 52 | // Validate age 53 | if let Err(err) = validator.get_rule("min_age") 54 | .unwrap() 55 | .validate_any(&self.age) { 56 | errors.entry("age".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 57 | } 58 | 59 | // Validate interests 60 | if let Err(err) = validator.get_rule("interests_required") 61 | .unwrap() 62 | .validate_any(&self.interests) { 63 | errors.entry("interests".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 64 | } 65 | 66 | // Check if there are any validation errors 67 | if !errors.is_empty() { 68 | return Err(ValidationError::Multiple(errors)); 69 | } 70 | 71 | Ok(()) 72 | } 73 | } 74 | 75 | fn main() { 76 | // Create a valid user 77 | let valid_user = User { 78 | username: "johndoe".to_string(), 79 | email: "john@example.com".to_string(), 80 | age: 25, 81 | interests: vec!["coding".to_string(), "reading".to_string()], 82 | }; 83 | 84 | // Validate the user 85 | match valid_user.validate() { 86 | Ok(_) => println!("Valid user: {}", valid_user.username), 87 | Err(err) => println!("Validation failed: {}", err), 88 | } 89 | 90 | // Create an invalid user 91 | let invalid_user = User { 92 | username: "jo".to_string(), 93 | email: "invalid-email".to_string(), 94 | age: 16, 95 | interests: vec![], 96 | }; 97 | 98 | // Validate the invalid user 99 | match invalid_user.validate() { 100 | Ok(_) => println!("Valid user: {}", invalid_user.username), 101 | Err(err) => println!("Validation failed: {}", err), 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/attribute_validation.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::collections::HashMap; 3 | 4 | use rustvalidity::prelude::*; 5 | use rustvalidity::error::ValidationError; 6 | use rustvalidity::validator::{Validator, Validate}; 7 | use rustvalidity::rules::{Rule, common, numeric, collection, advanced}; 8 | 9 | #[derive(Debug)] 10 | struct Product { 11 | // #[validate(required, length(3, 50))] 12 | name: String, 13 | 14 | // #[validate(min(0))] 15 | price: f64, 16 | 17 | // #[validate(required, email)] 18 | contact_email: String, 19 | 20 | // #[validate(min_size(1), each(required))] 21 | categories: Vec, 22 | 23 | // #[validate(url)] 24 | website: String, 25 | } 26 | 27 | impl Validate for Product { 28 | fn validate(&self) -> Result<(), ValidationError> { 29 | let mut validator = Validator::new(); 30 | 31 | // Register validation rules 32 | validator.add_rule("required", common::Required); 33 | validator.add_rule("name_length", common::Length { min: 3, max: Some(50) }); 34 | validator.add_rule("min_price", numeric::Min { value: 0.0 }); 35 | validator.add_rule("email", common::Email { check_dns: false }); 36 | validator.add_rule("categories_required", collection::MinSize { min: 1 }); 37 | validator.add_rule("url", common::Url { allowed_schemes: Some(vec!["http".to_string(), "https".to_string()]) }); 38 | 39 | // Validate fields 40 | let mut errors = HashMap::new(); 41 | 42 | // Validate name (required, length between 3 and 50) 43 | if let Err(err) = validator.get_rule("required").unwrap().validate(&self.name as &dyn Any) { 44 | errors.entry("name".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 45 | } else if let Err(err) = validator.get_rule("name_length").unwrap().validate(&self.name as &dyn Any) { 46 | errors.entry("name".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 47 | } 48 | 49 | // Validate price (min 0) 50 | if let Err(err) = validator.get_rule("min_price").unwrap().validate(&self.price as &dyn Any) { 51 | errors.entry("price".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 52 | } 53 | 54 | // Validate contact_email (required, email format) 55 | if let Err(err) = validator.get_rule("required").unwrap().validate(&self.contact_email as &dyn Any) { 56 | errors.entry("contact_email".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 57 | } else if let Err(err) = validator.get_rule("email").unwrap().validate(&self.contact_email as &dyn Any) { 58 | errors.entry("contact_email".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 59 | } 60 | 61 | // Validate categories (min_size 1) 62 | if let Err(err) = validator.get_rule("categories_required").unwrap().validate(&self.categories as &dyn Any) { 63 | errors.entry("categories".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 64 | } 65 | 66 | // Validate each category (required) 67 | for (i, category) in self.categories.iter().enumerate() { 68 | if let Err(err) = validator.get_rule("required").unwrap().validate(category as &dyn Any) { 69 | errors.entry(format!("categories[{}]", i)).or_insert_with(Vec::new).push(format!("{}", err)); 70 | } 71 | } 72 | 73 | // Validate website (url format) 74 | if !self.website.is_empty() { 75 | if let Err(err) = validator.get_rule("url").unwrap().validate(&self.website as &dyn Any) { 76 | errors.entry("website".to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 77 | } 78 | } 79 | 80 | // Return errors if any 81 | if !errors.is_empty() { 82 | return Err(ValidationError::Multiple(errors)); 83 | } 84 | 85 | Ok(()) 86 | } 87 | } 88 | 89 | fn main() { 90 | // Create a valid product 91 | let valid_product = Product { 92 | name: "Awesome Product".to_string(), 93 | price: 29.99, 94 | contact_email: "sales@example.com".to_string(), 95 | categories: vec!["Electronics".to_string(), "Gadgets".to_string()], 96 | website: "https://example.com/products".to_string(), 97 | }; 98 | 99 | // Validate the product 100 | match valid_product.validate() { 101 | Ok(_) => println!("Valid product: {}", valid_product.name), 102 | Err(err) => println!("Validation failed: {}", err), 103 | } 104 | 105 | // Create an invalid product 106 | let invalid_product = Product { 107 | name: "Ab".to_string(), 108 | price: -10.0, 109 | contact_email: "not-an-email".to_string(), 110 | categories: vec![], 111 | website: "not-a-url".to_string(), 112 | }; 113 | 114 | // Validate the invalid product 115 | match invalid_product.validate() { 116 | Ok(_) => println!("Valid product: {}", invalid_product.name), 117 | Err(err) => println!("Validation failed: {}", err), 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RustValidity - Rust Object Validator 2 | 3 | RustValidity is a powerful, flexible, and easy-to-use validation library for Rust that provides struct-level validation. It offers a wide range of built-in validation rules and supports custom validation logic. 4 | 5 | ## Author 6 | 7 | **Saeed Ghanbari** - [GitHub](https://github.com/sgh370) 8 | 9 | ## Features 10 | 11 | - **Comprehensive Rule Set**: Includes common validations for strings, numbers, collections, and more 12 | - **Extensible**: Easily create custom validation rules 13 | - **Flexible Error Handling**: Support for single and multiple validation errors 14 | - **Attribute-Based Validation**: Optional derive macro for struct field validation (with the `derive` feature) 15 | - **Conditional Validation**: Rules that apply only under specific conditions 16 | - **Cross-field Validation**: Validate fields based on the values of other fields 17 | - **Collection Validation**: Validate arrays, vectors, maps, and other collections 18 | - Multiple error handling 19 | - Nested struct validation 20 | 21 | ## Installation 22 | 23 | Add this to your `Cargo.toml`: 24 | 25 | ```toml 26 | [dependencies] 27 | rustvalidity = "0.1.0" 28 | ``` 29 | 30 | ## Quick Start 31 | 32 | ```rust 33 | use rustvalidity::error::ValidationError; 34 | use rustvalidity::validator::{Validator, Validate}; 35 | use rustvalidity::rules::{common, numeric, collection}; 36 | 37 | struct User { 38 | username: String, 39 | email: String, 40 | age: i32, 41 | interests: Vec, 42 | } 43 | 44 | impl Validate for User { 45 | fn validate(&self) -> Result<(), ValidationError> { 46 | // Create a new validator instance 47 | let mut validator = Validator::new(); 48 | 49 | // Add validation rules 50 | validator.add_rule("required", common::Required); 51 | validator.add_rule("username_length", common::Length { min: 3, max: Some(20) }); 52 | validator.add_rule("email", common::Email { check_dns: false }); 53 | validator.add_rule("min_age", numeric::Min { value: 18 }); 54 | validator.add_rule("interests_required", collection::MinSize { min: 1 }); 55 | 56 | // Validate fields 57 | if let Err(err) = validator.get_rule("required").unwrap().validate_any(&self.username) { 58 | return Err(err); 59 | } 60 | 61 | if let Err(err) = validator.get_rule("username_length").unwrap().validate_any(&self.username) { 62 | return Err(err); 63 | } 64 | 65 | if let Err(err) = validator.get_rule("email").unwrap().validate_any(&self.email) { 66 | return Err(err); 67 | } 68 | 69 | if let Err(err) = validator.get_rule("min_age").unwrap().validate_any(&self.age) { 70 | return Err(err); 71 | } 72 | 73 | if let Err(err) = validator.get_rule("interests_required").unwrap().validate_any(&self.interests) { 74 | return Err(err); 75 | } 76 | 77 | Ok(()) 78 | } 79 | } 80 | 81 | fn main() { 82 | let user = User { 83 | username: "johndoe".to_string(), 84 | email: "john@example.com".to_string(), 85 | age: 25, 86 | interests: vec!["coding".to_string(), "reading".to_string()], 87 | }; 88 | 89 | match user.validate() { 90 | Ok(_) => println!("User is valid!"), 91 | Err(err) => println!("Validation failed: {}", err), 92 | } 93 | } 94 | ``` 95 | 96 | ## Available Validation Rules 97 | 98 | ### Common Rules 99 | 100 | - `Required`: Validates that a value is not empty 101 | - `Length`: Validates string length (min, max) 102 | - `Email`: Validates email format 103 | - `UrlRule`: Validates URL format with optional scheme restrictions 104 | - `UuidRule`: Validates UUID format 105 | - `Json`: Validates JSON format 106 | - `Date`: Validates date format and range 107 | - `Phone`: Validates phone number format 108 | - `OneOf`: Validates that a value is one of a set of allowed values 109 | - `Custom`: Create custom validation rules with closures 110 | 111 | ### Numeric Rules 112 | 113 | - `Min`: Validates minimum value 114 | - `Max`: Validates maximum value 115 | - `Range`: Validates value within a range 116 | - `Positive`: Validates positive numbers 117 | - `Negative`: Validates negative numbers 118 | - `DivisibleBy`: Validates divisibility 119 | 120 | ### Collection Rules 121 | 122 | - `Unique`: Validates collection elements are unique 123 | - `Contains`: Validates collection contains a specific value 124 | - `Each`: Applies a validation rule to each element 125 | - `Map`: Validates map keys and values 126 | - `MinSize`: Validates minimum collection size 127 | - `MaxSize`: Validates maximum collection size 128 | - `ExactSize`: Validates exact collection size 129 | 130 | ### Conditional Rules 131 | 132 | - `If`: Validates a value only if a condition is true 133 | - `Unless`: Validates a value only if a condition is false 134 | - `RequiredIf`: Validates that a value is required if a condition is true 135 | - `RequiredWith`: Validates that a value is required if another field has a specific value 136 | - `RequiredWithout`: Validates that a value is required if another field does not have a specific value 137 | - `RequiredIfAny`: Validates that a value is required if any of the specified conditions are true 138 | - `RequiredIfAll`: Validates that a value is required if all of the specified conditions are true 139 | 140 | ### Advanced Rules 141 | 142 | - `Password`: Validates password complexity 143 | - `CreditCard`: Validates credit card numbers 144 | - `SemVer`: Validates semantic version strings 145 | - `Domain`: Validates domain names 146 | - `Port`: Validates port numbers 147 | - `IP`: Validates IP addresses 148 | - `RegexRule`: Validates against a regular expression 149 | 150 | ## Custom Validation Rules 151 | 152 | You can create custom validation rules by implementing the `Rule` trait: 153 | 154 | ```rust 155 | use rustvalidity::error::ValidationError; 156 | use rustvalidity::rules::Rule; 157 | 158 | struct MyCustomRule; 159 | 160 | impl Rule for MyCustomRule { 161 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 162 | // Your custom validation logic here 163 | Ok(()) 164 | } 165 | } 166 | ``` 167 | 168 | ## Error Handling 169 | 170 | Rustvalidity provides two types of validation errors: 171 | 172 | 1. `ValidationError::Single` - A single validation error with a message 173 | 2. `ValidationError::Multiple` - Multiple validation errors grouped by field 174 | 175 | You can collect all validation errors using the `validate_all` method: 176 | 177 | ```rust 178 | let errors = validator.validate_all(&value); 179 | ``` 180 | 181 | ## Examples 182 | 183 | Check out the examples directory for more usage examples: 184 | 185 | - `user_validation.rs` - Basic validation example 186 | - `attribute_validation.rs` - Advanced validation with struct attributes 187 | - 188 | 189 | ## Contributing 190 | 191 | Contributions are welcome! Please feel free to submit a Pull Request. 192 | 193 | 1. Fork the repository 194 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 195 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 196 | 4. Push to the branch (`git push origin feature/amazing-feature`) 197 | 5. Open a Pull Request 198 | 199 | ## License 200 | 201 | This project is licensed under the MIT License - see the LICENSE file for details. 202 | -------------------------------------------------------------------------------- /src/rules/numeric.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::error::ValidationError; 4 | use crate::rules::Rule; 5 | 6 | /// Validates that a numeric value is within a specified range 7 | pub struct Range { 8 | pub min: T, 9 | pub max: T, 10 | } 11 | 12 | impl Rule for Range { 13 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 14 | if let Some(val) = value.downcast_ref::() { 15 | if *val < self.min { 16 | return Err(ValidationError::new(format!( 17 | "Value must be greater than or equal to {:?}", self.min 18 | ))); 19 | } 20 | if *val > self.max { 21 | return Err(ValidationError::new(format!( 22 | "Value must be less than or equal to {:?}", self.max 23 | ))); 24 | } 25 | Ok(()) 26 | } else { 27 | Err(ValidationError::new(format!( 28 | "Value is not of the expected numeric type" 29 | ))) 30 | } 31 | } 32 | } 33 | 34 | /// Validates that a numeric value is positive 35 | pub struct Positive; 36 | 37 | impl Rule for Positive { 38 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 39 | if let Some(val) = value.downcast_ref::() { 40 | if *val <= 0 { 41 | return Err(ValidationError::new("Value must be positive")); 42 | } 43 | } else if let Some(val) = value.downcast_ref::() { 44 | if *val <= 0 { 45 | return Err(ValidationError::new("Value must be positive")); 46 | } 47 | } else if let Some(val) = value.downcast_ref::() { 48 | if *val <= 0 { 49 | return Err(ValidationError::new("Value must be positive")); 50 | } 51 | } else if let Some(val) = value.downcast_ref::() { 52 | if *val <= 0 { 53 | return Err(ValidationError::new("Value must be positive")); 54 | } 55 | } else if let Some(val) = value.downcast_ref::() { 56 | if *val <= 0.0 { 57 | return Err(ValidationError::new("Value must be positive")); 58 | } 59 | } else if let Some(val) = value.downcast_ref::() { 60 | if *val <= 0.0 { 61 | return Err(ValidationError::new("Value must be positive")); 62 | } 63 | } else if let Some(val) = value.downcast_ref::() { 64 | if *val == 0 { 65 | return Err(ValidationError::new("Value must be positive")); 66 | } 67 | } else if let Some(val) = value.downcast_ref::() { 68 | if *val == 0 { 69 | return Err(ValidationError::new("Value must be positive")); 70 | } 71 | } else if let Some(val) = value.downcast_ref::() { 72 | if *val == 0 { 73 | return Err(ValidationError::new("Value must be positive")); 74 | } 75 | } else if let Some(val) = value.downcast_ref::() { 76 | if *val == 0 { 77 | return Err(ValidationError::new("Value must be positive")); 78 | } 79 | } else if let Some(val) = value.downcast_ref::() { 80 | if *val == 0 { 81 | return Err(ValidationError::new("Value must be positive")); 82 | } 83 | } else { 84 | return Err(ValidationError::new("Value is not a numeric type")); 85 | } 86 | 87 | Ok(()) 88 | } 89 | } 90 | 91 | /// Validates that a numeric value is greater than or equal to a minimum value 92 | pub struct Min { 93 | pub value: T, 94 | } 95 | 96 | impl Rule for Min { 97 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 98 | if let Some(val) = value.downcast_ref::() { 99 | if *val < self.value { 100 | return Err(ValidationError::new(format!( 101 | "Value must be greater than or equal to {:?}", self.value 102 | ))); 103 | } 104 | Ok(()) 105 | } else { 106 | Err(ValidationError::new("Value is not of the expected numeric type")) 107 | } 108 | } 109 | } 110 | 111 | /// Validates that a numeric value is less than or equal to a maximum value 112 | pub struct Max { 113 | pub value: T, 114 | } 115 | 116 | impl Rule for Max { 117 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 118 | if let Some(val) = value.downcast_ref::() { 119 | if *val > self.value { 120 | return Err(ValidationError::new(format!( 121 | "Value must be less than or equal to {:?}", self.value 122 | ))); 123 | } 124 | Ok(()) 125 | } else { 126 | Err(ValidationError::new(format!( 127 | "Value is not of the expected numeric type" 128 | ))) 129 | } 130 | } 131 | } 132 | 133 | /// Validates that a numeric value is negative 134 | pub struct Negative; 135 | 136 | impl Rule for Negative { 137 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 138 | if let Some(val) = value.downcast_ref::() { 139 | if *val >= 0 { 140 | return Err(ValidationError::new("Value must be negative")); 141 | } 142 | } else if let Some(val) = value.downcast_ref::() { 143 | if *val >= 0 { 144 | return Err(ValidationError::new("Value must be negative")); 145 | } 146 | } else if let Some(val) = value.downcast_ref::() { 147 | if *val >= 0 { 148 | return Err(ValidationError::new("Value must be negative")); 149 | } 150 | } else if let Some(val) = value.downcast_ref::() { 151 | if *val >= 0 { 152 | return Err(ValidationError::new("Value must be negative")); 153 | } 154 | } else if let Some(val) = value.downcast_ref::() { 155 | if *val >= 0.0 { 156 | return Err(ValidationError::new("Value must be negative")); 157 | } 158 | } else if let Some(val) = value.downcast_ref::() { 159 | if *val >= 0.0 { 160 | return Err(ValidationError::new("Value must be negative")); 161 | } 162 | } else { 163 | return Err(ValidationError::new("Value is not a signed numeric type")); 164 | } 165 | 166 | Ok(()) 167 | } 168 | } 169 | 170 | /// Validates that a numeric value is divisible by another value 171 | pub struct DivisibleBy { 172 | pub divisor: T, 173 | } 174 | 175 | impl Rule for DivisibleBy { 176 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 177 | if let Some(val) = value.downcast_ref::() { 178 | if self.divisor == 0 { 179 | return Err(ValidationError::new("Divisor cannot be zero")); 180 | } 181 | if *val % self.divisor != 0 { 182 | return Err(ValidationError::new(format!( 183 | "Value must be divisible by {}", self.divisor 184 | ))); 185 | } 186 | Ok(()) 187 | } else { 188 | Err(ValidationError::new("Value is not of the expected numeric type")) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/rules/conditional.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ValidationError; 2 | use crate::rules::Rule; 3 | 4 | /// Validates a value only if a condition is true 5 | pub struct If { 6 | pub condition: Box Fn(&'a dyn std::any::Any) -> bool + Send + Sync>, 7 | pub then: Box, 8 | } 9 | 10 | impl Rule for If { 11 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 12 | if (self.condition)(value) { 13 | self.then.validate_any(value) 14 | } else { 15 | Ok(()) 16 | } 17 | } 18 | } 19 | 20 | /// Validates a value only if a condition is false 21 | pub struct Unless { 22 | pub condition: Box Fn(&'a dyn std::any::Any) -> bool + Send + Sync>, 23 | pub then: Box, 24 | } 25 | 26 | impl Rule for Unless { 27 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 28 | if !(self.condition)(value) { 29 | self.then.validate_any(value) 30 | } else { 31 | Ok(()) 32 | } 33 | } 34 | } 35 | 36 | /// Validates that a value is required if a condition is true 37 | pub struct RequiredIf { 38 | pub condition: Box bool + Send + Sync>, 39 | } 40 | 41 | impl Rule for RequiredIf { 42 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 43 | if (self.condition)() { 44 | // Check if value is empty or null 45 | if let Some(s) = value.downcast_ref::() { 46 | if s.is_empty() { 47 | return Err(ValidationError::new("Value is required")); 48 | } 49 | } else if let Some(s) = value.downcast_ref::<&str>() { 50 | if s.is_empty() { 51 | return Err(ValidationError::new("Value is required")); 52 | } 53 | } else if let Some(o) = value.downcast_ref::>() { 54 | if o.is_none() { 55 | return Err(ValidationError::new("Value is required")); 56 | } 57 | } else if let Some(v) = value.downcast_ref::>() { 58 | if v.is_empty() { 59 | return Err(ValidationError::new("Value is required")); 60 | } 61 | } 62 | } 63 | 64 | Ok(()) 65 | } 66 | } 67 | 68 | /// Validates that a value is required if another field has a specific value 69 | pub struct RequiredWith { 70 | pub other_field: Box Option + Send + Sync>, 71 | pub expected_value: T, 72 | } 73 | 74 | impl Rule for RequiredWith { 75 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 76 | if let Some(other_value) = (self.other_field)() { 77 | if other_value == self.expected_value { 78 | // Check if value is empty or null 79 | if let Some(s) = value.downcast_ref::() { 80 | if s.is_empty() { 81 | return Err(ValidationError::new("Value is required")); 82 | } 83 | } else if let Some(s) = value.downcast_ref::<&str>() { 84 | if s.is_empty() { 85 | return Err(ValidationError::new("Value is required")); 86 | } 87 | } else if let Some(o) = value.downcast_ref::>() { 88 | if o.is_none() { 89 | return Err(ValidationError::new("Value is required")); 90 | } 91 | } else if let Some(v) = value.downcast_ref::>() { 92 | if v.is_empty() { 93 | return Err(ValidationError::new("Value is required")); 94 | } 95 | } 96 | } 97 | } 98 | 99 | Ok(()) 100 | } 101 | } 102 | 103 | /// Validates that a value is required if another field does not have a specific value 104 | pub struct RequiredWithout { 105 | pub other_field: Box Option + Send + Sync>, 106 | pub expected_value: T, 107 | } 108 | 109 | impl Rule for RequiredWithout { 110 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 111 | if let Some(other_value) = (self.other_field)() { 112 | if other_value != self.expected_value { 113 | // Check if value is empty or null 114 | if let Some(s) = value.downcast_ref::() { 115 | if s.is_empty() { 116 | return Err(ValidationError::new("Value is required")); 117 | } 118 | } else if let Some(s) = value.downcast_ref::<&str>() { 119 | if s.is_empty() { 120 | return Err(ValidationError::new("Value is required")); 121 | } 122 | } else if let Some(o) = value.downcast_ref::>() { 123 | if o.is_none() { 124 | return Err(ValidationError::new("Value is required")); 125 | } 126 | } else if let Some(v) = value.downcast_ref::>() { 127 | if v.is_empty() { 128 | return Err(ValidationError::new("Value is required")); 129 | } 130 | } 131 | } 132 | } 133 | 134 | Ok(()) 135 | } 136 | } 137 | 138 | /// Validates that a value is required if any of the specified conditions are true 139 | pub struct RequiredIfAny { 140 | pub conditions: Vec bool + Send + Sync>>, 141 | } 142 | 143 | impl Rule for RequiredIfAny { 144 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 145 | if self.conditions.iter().any(|condition| condition()) { 146 | // Check if value is empty or null 147 | if let Some(s) = value.downcast_ref::() { 148 | if s.is_empty() { 149 | return Err(ValidationError::new("Value is required")); 150 | } 151 | } else if let Some(s) = value.downcast_ref::<&str>() { 152 | if s.is_empty() { 153 | return Err(ValidationError::new("Value is required")); 154 | } 155 | } else if let Some(o) = value.downcast_ref::>() { 156 | if o.is_none() { 157 | return Err(ValidationError::new("Value is required")); 158 | } 159 | } else if let Some(v) = value.downcast_ref::>() { 160 | if v.is_empty() { 161 | return Err(ValidationError::new("Value is required")); 162 | } 163 | } 164 | } 165 | 166 | Ok(()) 167 | } 168 | } 169 | 170 | /// Validates that a value is required if all of the specified conditions are true 171 | pub struct RequiredIfAll { 172 | pub conditions: Vec bool + Send + Sync>>, 173 | } 174 | 175 | impl Rule for RequiredIfAll { 176 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 177 | if self.conditions.iter().all(|condition| condition()) { 178 | // Check if value is empty or null 179 | if let Some(s) = value.downcast_ref::() { 180 | if s.is_empty() { 181 | return Err(ValidationError::new("Value is required")); 182 | } 183 | } else if let Some(s) = value.downcast_ref::<&str>() { 184 | if s.is_empty() { 185 | return Err(ValidationError::new("Value is required")); 186 | } 187 | } else if let Some(o) = value.downcast_ref::>() { 188 | if o.is_none() { 189 | return Err(ValidationError::new("Value is required")); 190 | } 191 | } else if let Some(v) = value.downcast_ref::>() { 192 | if v.is_empty() { 193 | return Err(ValidationError::new("Value is required")); 194 | } 195 | } 196 | } 197 | 198 | Ok(()) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/rules/advanced.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | use crate::error::ValidationError; 4 | use crate::rules::Rule; 5 | 6 | /// Validates password complexity 7 | pub struct Password { 8 | pub min_length: usize, 9 | pub require_uppercase: bool, 10 | pub require_lowercase: bool, 11 | pub require_digit: bool, 12 | pub require_special: bool, 13 | } 14 | 15 | impl Rule for Password { 16 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 17 | if let Some(s) = value.downcast_ref::() { 18 | if s.len() < self.min_length { 19 | return Err(ValidationError::new(format!( 20 | "Password must be at least {} characters long", self.min_length 21 | ))); 22 | } 23 | 24 | if self.require_uppercase && !s.chars().any(|c| c.is_uppercase()) { 25 | return Err(ValidationError::new( 26 | "Password must contain at least one uppercase letter" 27 | )); 28 | } 29 | 30 | if self.require_lowercase && !s.chars().any(|c| c.is_lowercase()) { 31 | return Err(ValidationError::new( 32 | "Password must contain at least one lowercase letter" 33 | )); 34 | } 35 | 36 | if self.require_digit && !s.chars().any(|c| c.is_digit(10)) { 37 | return Err(ValidationError::new( 38 | "Password must contain at least one digit" 39 | )); 40 | } 41 | 42 | if self.require_special && !s.chars().any(|c| !c.is_alphanumeric()) { 43 | return Err(ValidationError::new( 44 | "Password must contain at least one special character" 45 | )); 46 | } 47 | 48 | Ok(()) 49 | } else { 50 | Err(ValidationError::new("Value must be a string")) 51 | } 52 | } 53 | } 54 | 55 | /// Validates credit card numbers 56 | pub struct CreditCard; 57 | 58 | impl Rule for CreditCard { 59 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 60 | if let Some(s) = value.downcast_ref::() { 61 | validate_credit_card(s) 62 | } else if let Some(s) = value.downcast_ref::<&str>() { 63 | validate_credit_card(s) 64 | } else { 65 | Err(ValidationError::new("Value must be a string")) 66 | } 67 | } 68 | } 69 | 70 | fn validate_credit_card(card: &str) -> Result<(), ValidationError> { 71 | // Remove spaces and dashes 72 | let card = card.replace([' ', '-'], ""); 73 | 74 | // Check if the card number contains only digits 75 | if !card.chars().all(|c| c.is_digit(10)) { 76 | return Err(ValidationError::new("Credit card number must contain only digits")); 77 | } 78 | 79 | // Check length (most cards are 13-19 digits) 80 | if card.len() < 13 || card.len() > 19 { 81 | return Err(ValidationError::new("Credit card number has invalid length")); 82 | } 83 | 84 | // Luhn algorithm validation 85 | let mut sum = 0; 86 | let mut double = false; 87 | 88 | for c in card.chars().rev() { 89 | let mut digit = c.to_digit(10).unwrap(); 90 | 91 | if double { 92 | digit *= 2; 93 | if digit > 9 { 94 | digit -= 9; 95 | } 96 | } 97 | 98 | sum += digit; 99 | double = !double; 100 | } 101 | 102 | if sum % 10 != 0 { 103 | return Err(ValidationError::new("Invalid credit card number")); 104 | } 105 | 106 | Ok(()) 107 | } 108 | 109 | /// Validates semantic version strings 110 | pub struct SemVer; 111 | 112 | impl Rule for SemVer { 113 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 114 | if let Some(s) = value.downcast_ref::() { 115 | validate_semver(s) 116 | } else if let Some(s) = value.downcast_ref::<&str>() { 117 | validate_semver(s) 118 | } else { 119 | Err(ValidationError::new("Value must be a string")) 120 | } 121 | } 122 | } 123 | 124 | fn validate_semver(version: &str) -> Result<(), ValidationError> { 125 | let semver_regex = regex::Regex::new(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$").unwrap(); 126 | 127 | if !semver_regex.is_match(version) { 128 | return Err(ValidationError::new("Invalid semantic version format")); 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | /// Validates domain names 135 | pub struct Domain; 136 | 137 | impl Rule for Domain { 138 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 139 | if let Some(s) = value.downcast_ref::() { 140 | validate_domain(s) 141 | } else if let Some(s) = value.downcast_ref::<&str>() { 142 | validate_domain(s) 143 | } else { 144 | Err(ValidationError::new("Value must be a string")) 145 | } 146 | } 147 | } 148 | 149 | fn validate_domain(domain: &str) -> Result<(), ValidationError> { 150 | let domain_regex = regex::Regex::new(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]$").unwrap(); 151 | 152 | if !domain_regex.is_match(domain) { 153 | return Err(ValidationError::new("Invalid domain name format")); 154 | } 155 | 156 | Ok(()) 157 | } 158 | 159 | /// Validates port numbers 160 | pub struct Port; 161 | 162 | impl Rule for Port { 163 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 164 | if let Some(port) = value.downcast_ref::() { 165 | if *port == 0 { 166 | return Err(ValidationError::new("Port number cannot be 0")); 167 | } 168 | Ok(()) 169 | } else if let Some(port) = value.downcast_ref::() { 170 | if *port <= 0 || *port > 65535 { 171 | return Err(ValidationError::new("Port number must be between 1 and 65535")); 172 | } 173 | Ok(()) 174 | } else if let Some(s) = value.downcast_ref::() { 175 | match s.parse::() { 176 | Ok(port) => { 177 | if port == 0 { 178 | return Err(ValidationError::new("Port number cannot be 0")); 179 | } 180 | Ok(()) 181 | }, 182 | Err(_) => Err(ValidationError::new("Invalid port number format")), 183 | } 184 | } else if let Some(s) = value.downcast_ref::<&str>() { 185 | match s.parse::() { 186 | Ok(port) => { 187 | if port == 0 { 188 | return Err(ValidationError::new("Port number cannot be 0")); 189 | } 190 | Ok(()) 191 | }, 192 | Err(_) => Err(ValidationError::new("Invalid port number format")), 193 | } 194 | } else { 195 | Err(ValidationError::new("Value must be a port number (u16, i32, or string)")) 196 | } 197 | } 198 | } 199 | 200 | /// Validates IP addresses 201 | pub struct IP { 202 | pub allow_v4: bool, 203 | pub allow_v6: bool, 204 | } 205 | 206 | impl Rule for IP { 207 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 208 | if let Some(s) = value.downcast_ref::() { 209 | validate_ip(s, self) 210 | } else if let Some(s) = value.downcast_ref::<&str>() { 211 | validate_ip(s, self) 212 | } else { 213 | Err(ValidationError::new("Value must be a string")) 214 | } 215 | } 216 | } 217 | 218 | fn validate_ip(ip: &str, ip_rule: &IP) -> Result<(), ValidationError> { 219 | let ipv4_regex = regex::Regex::new(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$").unwrap(); 220 | let ipv6_regex = regex::Regex::new(r"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$").unwrap(); 221 | 222 | let is_ipv4 = ipv4_regex.is_match(ip); 223 | let is_ipv6 = ipv6_regex.is_match(ip); 224 | 225 | if (is_ipv4 && ip_rule.allow_v4) || (is_ipv6 && ip_rule.allow_v6) { 226 | Ok(()) 227 | } else if is_ipv4 && !ip_rule.allow_v4 { 228 | Err(ValidationError::new("IPv4 addresses are not allowed")) 229 | } else if is_ipv6 && !ip_rule.allow_v6 { 230 | Err(ValidationError::new("IPv6 addresses are not allowed")) 231 | } else { 232 | Err(ValidationError::new("Invalid IP address format")) 233 | } 234 | } 235 | 236 | /// Validates against a regular expression 237 | pub struct RegexRule { 238 | pub pattern: String, 239 | pub regex: regex::Regex, 240 | } 241 | 242 | impl RegexRule { 243 | pub fn new(pattern: &str) -> Result { 244 | match regex::Regex::new(pattern) { 245 | Ok(regex) => Ok(RegexRule { 246 | pattern: pattern.to_string(), 247 | regex, 248 | }), 249 | Err(_) => Err(ValidationError::new("Invalid regex pattern")), 250 | } 251 | } 252 | } 253 | 254 | impl Rule for RegexRule { 255 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 256 | if let Some(s) = value.downcast_ref::() { 257 | if !self.regex.is_match(s) { 258 | return Err(ValidationError::new(format!( 259 | "Value does not match pattern: {}", self.pattern 260 | ))); 261 | } 262 | } else if let Some(s) = value.downcast_ref::<&str>() { 263 | if !self.regex.is_match(s) { 264 | return Err(ValidationError::new(format!( 265 | "Value does not match pattern: {}", self.pattern 266 | ))); 267 | } 268 | } else { 269 | return Err(ValidationError::new("Value must be a string")); 270 | } 271 | 272 | Ok(()) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/rules/collection.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::hash::Hash; 3 | 4 | use crate::error::ValidationError; 5 | use crate::rules::Rule; 6 | 7 | /// Validates that all elements in a collection are unique 8 | pub struct Unique; 9 | 10 | impl Rule for Unique { 11 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 12 | // For Vec where T: Eq + Hash 13 | if let Some(vec) = value.downcast_ref::>() { 14 | let mut set = HashSet::new(); 15 | for item in vec { 16 | if !set.insert(item) { 17 | return Err(ValidationError::new(format!( 18 | "Duplicate value found: {}", item 19 | ))); 20 | } 21 | } 22 | } else if let Some(vec) = value.downcast_ref::>() { 23 | let mut set = HashSet::new(); 24 | for item in vec { 25 | if !set.insert(*item) { 26 | return Err(ValidationError::new(format!( 27 | "Duplicate value found: {}", item 28 | ))); 29 | } 30 | } 31 | } else if let Some(vec) = value.downcast_ref::>() { 32 | let mut set = HashSet::new(); 33 | for item in vec { 34 | if !set.insert(*item) { 35 | return Err(ValidationError::new(format!( 36 | "Duplicate value found: {}", item 37 | ))); 38 | } 39 | } 40 | } else { 41 | return Err(ValidationError::new( 42 | "Value must be a collection of hashable items" 43 | )); 44 | } 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | /// Validates that a collection contains a specific value 51 | pub struct Contains { 52 | pub value: T, 53 | } 54 | 55 | impl Rule for Contains { 56 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 57 | if let Some(vec) = value.downcast_ref::>() { 58 | if !vec.contains(&self.value) { 59 | return Err(ValidationError::new(format!( 60 | "Collection must contain {:?}", self.value 61 | ))); 62 | } 63 | } else { 64 | return Err(ValidationError::new( 65 | "Value must be a collection of the expected type" 66 | )); 67 | } 68 | 69 | Ok(()) 70 | } 71 | } 72 | 73 | /// Applies a validation rule to each element in a collection 74 | pub struct Each { 75 | pub rule: Box, 76 | } 77 | 78 | impl Rule for Each { 79 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 80 | if let Some(vec) = value.downcast_ref::>() { 81 | for (i, item) in vec.iter().enumerate() { 82 | if let Err(err) = self.rule.validate_any(item) { 83 | return Err(ValidationError::new(format!( 84 | "Item at index {} failed validation: {}", i, err 85 | ))); 86 | } 87 | } 88 | } else if let Some(vec) = value.downcast_ref::>() { 89 | for (i, item) in vec.iter().enumerate() { 90 | if let Err(err) = self.rule.validate_any(item) { 91 | return Err(ValidationError::new(format!( 92 | "Item at index {} failed validation: {}", i, err 93 | ))); 94 | } 95 | } 96 | } else if let Some(map) = value.downcast_ref::>() { 97 | for (key, val) in map { 98 | if let Err(err) = self.rule.validate_any(val) { 99 | return Err(ValidationError::new(format!( 100 | "Value for key '{}' failed validation: {}", key, err 101 | ))); 102 | } 103 | } 104 | } else { 105 | return Err(ValidationError::new("Value must be a collection or map")); 106 | } 107 | 108 | Ok(()) 109 | } 110 | } 111 | 112 | /// Validates a map's keys and values 113 | pub struct Map { 114 | pub key_rule: Option>, 115 | pub value_rule: Option>, 116 | } 117 | 118 | impl Rule for Map { 119 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 120 | if let Some(map) = value.downcast_ref::>() { 121 | for (key, val) in map { 122 | if let Some(key_rule) = &self.key_rule { 123 | if let Err(err) = key_rule.validate_any(key) { 124 | return Err(ValidationError::new(format!( 125 | "Map key '{}' failed validation: {}", key, err 126 | ))); 127 | } 128 | } 129 | 130 | if let Some(value_rule) = &self.value_rule { 131 | if let Err(err) = value_rule.validate_any(val) { 132 | return Err(ValidationError::new(format!( 133 | "Map value for key '{}' failed validation: {}", key, err 134 | ))); 135 | } 136 | } 137 | } 138 | } else if let Some(map) = value.downcast_ref::>() { 139 | for (key, val) in map { 140 | if let Some(key_rule) = &self.key_rule { 141 | if let Err(err) = key_rule.validate_any(key) { 142 | return Err(ValidationError::new(format!( 143 | "Map key '{}' failed validation: {}", key, err 144 | ))); 145 | } 146 | } 147 | 148 | if let Some(value_rule) = &self.value_rule { 149 | if let Err(err) = value_rule.validate_any(val) { 150 | return Err(ValidationError::new(format!( 151 | "Map value for key '{}' failed validation: {}", key, err 152 | ))); 153 | } 154 | } 155 | } 156 | } else { 157 | return Err(ValidationError::new( 158 | "Value must be a map" 159 | )); 160 | } 161 | 162 | Ok(()) 163 | } 164 | } 165 | 166 | /// Validates that a collection has a minimum size 167 | pub struct MinSize { 168 | pub min: usize, 169 | } 170 | 171 | impl Rule for MinSize { 172 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 173 | if let Some(vec) = value.downcast_ref::>() { 174 | if vec.len() < self.min { 175 | return Err(ValidationError::new(format!( 176 | "Collection must have at least {} items", self.min 177 | ))); 178 | } 179 | } else if let Some(vec) = value.downcast_ref::>() { 180 | if vec.len() < self.min { 181 | return Err(ValidationError::new(format!( 182 | "Collection must have at least {} items", self.min 183 | ))); 184 | } 185 | } else if let Some(map) = value.downcast_ref::>() { 186 | if map.len() < self.min { 187 | return Err(ValidationError::new(format!( 188 | "Map must have at least {} entries", self.min 189 | ))); 190 | } 191 | } else if let Some(s) = value.downcast_ref::() { 192 | if s.len() < self.min { 193 | return Err(ValidationError::new(format!( 194 | "String must have at least {} characters", self.min 195 | ))); 196 | } 197 | } else { 198 | return Err(ValidationError::new( 199 | "Value must be a collection, map, or string" 200 | )); 201 | } 202 | 203 | Ok(()) 204 | } 205 | } 206 | 207 | /// Validates that a collection has a maximum size 208 | pub struct MaxSize { 209 | pub max: usize, 210 | } 211 | 212 | impl Rule for MaxSize { 213 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 214 | if let Some(vec) = value.downcast_ref::>() { 215 | if vec.len() > self.max { 216 | return Err(ValidationError::new(format!( 217 | "Collection must have at most {} items", self.max 218 | ))); 219 | } 220 | } else if let Some(vec) = value.downcast_ref::>() { 221 | if vec.len() > self.max { 222 | return Err(ValidationError::new(format!( 223 | "Collection must have at most {} items", self.max 224 | ))); 225 | } 226 | } else if let Some(map) = value.downcast_ref::>() { 227 | if map.len() > self.max { 228 | return Err(ValidationError::new(format!( 229 | "Map must have at most {} entries", self.max 230 | ))); 231 | } 232 | } else if let Some(s) = value.downcast_ref::() { 233 | if s.len() > self.max { 234 | return Err(ValidationError::new(format!( 235 | "String must have at most {} characters", self.max 236 | ))); 237 | } 238 | } else { 239 | return Err(ValidationError::new( 240 | "Value must be a collection, map, or string" 241 | )); 242 | } 243 | 244 | Ok(()) 245 | } 246 | } 247 | 248 | /// Validates that a collection has an exact size 249 | pub struct ExactSize { 250 | pub size: usize, 251 | } 252 | 253 | impl Rule for ExactSize { 254 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 255 | if let Some(vec) = value.downcast_ref::>() { 256 | if vec.len() != self.size { 257 | return Err(ValidationError::new(format!( 258 | "Collection must have exactly {} items", self.size 259 | ))); 260 | } 261 | } else if let Some(vec) = value.downcast_ref::>() { 262 | if vec.len() != self.size { 263 | return Err(ValidationError::new(format!( 264 | "Collection must have exactly {} items", self.size 265 | ))); 266 | } 267 | } else if let Some(map) = value.downcast_ref::>() { 268 | if map.len() != self.size { 269 | return Err(ValidationError::new(format!( 270 | "Map must have exactly {} entries", self.size 271 | ))); 272 | } 273 | } else if let Some(s) = value.downcast_ref::() { 274 | if s.len() != self.size { 275 | return Err(ValidationError::new(format!( 276 | "String must have exactly {} characters", self.size 277 | ))); 278 | } 279 | } else { 280 | return Err(ValidationError::new( 281 | "Value must be a collection, map, or string" 282 | )); 283 | } 284 | 285 | Ok(()) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/rules/common.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use regex::Regex; 3 | use chrono::{DateTime, NaiveDate}; 4 | use url::Url; 5 | use uuid::Uuid; 6 | use serde_json::Value; 7 | use std::fmt::Debug; 8 | 9 | use crate::error::ValidationError; 10 | use crate::rules::Rule; 11 | 12 | /// Validates that a value is not empty (strings, collections, options) 13 | pub struct Required; 14 | 15 | impl Rule for Required { 16 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 17 | // Handle String type 18 | if let Some(s) = value.downcast_ref::() { 19 | if s.is_empty() { 20 | return Err(ValidationError::new("Value is required")); 21 | } 22 | } 23 | // Handle &str type 24 | else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 25 | if s.is_empty() { 26 | return Err(ValidationError::new("Value is required")); 27 | } 28 | } 29 | // Handle Option types 30 | else if let Some(o) = (value as &dyn std::any::Any).downcast_ref::>() { 31 | if o.is_none() { 32 | return Err(ValidationError::new("Value is required")); 33 | } 34 | } 35 | // Handle Vec types 36 | else if let Some(v) = (value as &dyn std::any::Any).downcast_ref::>() { 37 | if v.is_empty() { 38 | return Err(ValidationError::new("Value is required")); 39 | } 40 | } 41 | 42 | Ok(()) 43 | } 44 | } 45 | 46 | /// Validates string length 47 | pub struct Length { 48 | pub min: usize, 49 | pub max: Option, 50 | } 51 | 52 | impl Rule for Length { 53 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 54 | // Handle String type 55 | if let Some(s) = value.downcast_ref::() { 56 | let len = s.len(); 57 | if len < self.min { 58 | return Err(ValidationError::new(format!("Length must be at least {}", self.min))); 59 | } 60 | if let Some(max) = self.max { 61 | if len > max { 62 | return Err(ValidationError::new(format!("Length must not exceed {}", max))); 63 | } 64 | } 65 | } 66 | // Handle &str type 67 | else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 68 | let len = s.len(); 69 | if len < self.min { 70 | return Err(ValidationError::new(format!("Length must be at least {}", self.min))); 71 | } 72 | if let Some(max) = self.max { 73 | if len > max { 74 | return Err(ValidationError::new(format!("Length must not exceed {}", max))); 75 | } 76 | } 77 | } 78 | // Handle Vec types 79 | else if let Some(v) = (value as &dyn std::any::Any).downcast_ref::>() { 80 | let len = v.len(); 81 | if len < self.min { 82 | return Err(ValidationError::new(format!("Collection must have at least {} items", self.min))); 83 | } 84 | if let Some(max) = self.max { 85 | if len > max { 86 | return Err(ValidationError::new(format!("Collection must not exceed {} items", max))); 87 | } 88 | } 89 | } else { 90 | return Err(ValidationError::new("Value must be a string or collection")); 91 | } 92 | 93 | Ok(()) 94 | } 95 | } 96 | 97 | /// Validates that a value is one of the specified options 98 | pub struct OneOf { 99 | pub values: Vec, 100 | } 101 | 102 | impl Rule for OneOf { 103 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 104 | if let Some(val) = value.downcast_ref::() { 105 | if !self.values.contains(val) { 106 | return Err(ValidationError::new(format!("Value must be one of the allowed options"))); 107 | } 108 | } else { 109 | return Err(ValidationError::new("Value is not of the expected type")); 110 | } 111 | 112 | Ok(()) 113 | } 114 | } 115 | 116 | /// Validates email format 117 | pub struct Email { 118 | pub check_dns: bool, 119 | } 120 | 121 | impl Rule for Email { 122 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 123 | if let Some(s) = value.downcast_ref::() { 124 | validate_email(s, self.check_dns) 125 | } else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 126 | validate_email(s, self.check_dns) 127 | } else { 128 | Err(ValidationError::new("Value must be a string")) 129 | } 130 | } 131 | } 132 | 133 | fn validate_email(email: &str, _check_dns: bool) -> Result<(), ValidationError> { 134 | // Basic email validation using regex 135 | let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); 136 | 137 | if !email_regex.is_match(email) { 138 | return Err(ValidationError::new("Invalid email format")); 139 | } 140 | 141 | // DNS validation would be implemented here if check_dns is true 142 | // For simplicity, we're skipping actual DNS validation 143 | 144 | Ok(()) 145 | } 146 | 147 | /// Validates URL format 148 | pub struct UrlRule { 149 | pub allowed_schemes: Option>, 150 | } 151 | 152 | impl Rule for UrlRule { 153 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 154 | if let Some(s) = value.downcast_ref::() { 155 | validate_url(s, &self.allowed_schemes) 156 | } else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 157 | validate_url(s, &self.allowed_schemes) 158 | } else { 159 | Err(ValidationError::new("Value must be a string")) 160 | } 161 | } 162 | } 163 | 164 | fn validate_url(url_str: &str, allowed_schemes: &Option>) -> Result<(), ValidationError> { 165 | match url::Url::parse(url_str) { 166 | Ok(url) => { 167 | if let Some(schemes) = allowed_schemes { 168 | if !schemes.contains(&url.scheme().to_string()) { 169 | return Err(ValidationError::new(format!( 170 | "URL scheme must be one of: {:?}", schemes 171 | ))); 172 | } 173 | } 174 | Ok(()) 175 | }, 176 | Err(_) => Err(ValidationError::new("Invalid URL format")), 177 | } 178 | } 179 | 180 | /// Validates JSON format 181 | pub struct Json; 182 | 183 | impl Rule for Json { 184 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 185 | if let Some(s) = value.downcast_ref::() { 186 | validate_json(s) 187 | } else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 188 | validate_json(s) 189 | } else { 190 | Err(ValidationError::new("Value must be a string")) 191 | } 192 | } 193 | } 194 | 195 | fn validate_json(json_str: &str) -> Result<(), ValidationError> { 196 | match serde_json::from_str::(json_str) { 197 | Ok(_) => Ok(()), 198 | Err(_) => Err(ValidationError::new("Invalid JSON format")), 199 | } 200 | } 201 | 202 | /// Validates UUID format 203 | pub struct UuidRule; 204 | 205 | impl Rule for UuidRule { 206 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 207 | if let Some(s) = value.downcast_ref::() { 208 | validate_uuid(s) 209 | } else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 210 | validate_uuid(s) 211 | } else { 212 | Err(ValidationError::new("Value must be a string")) 213 | } 214 | } 215 | } 216 | 217 | fn validate_uuid(uuid_str: &str) -> Result<(), ValidationError> { 218 | match uuid::Uuid::from_str(uuid_str) { 219 | Ok(_) => Ok(()), 220 | Err(_) => Err(ValidationError::new("Invalid UUID format")), 221 | } 222 | } 223 | 224 | /// Validates date format 225 | pub struct Date { 226 | pub format: String, 227 | pub min: Option, 228 | pub max: Option, 229 | } 230 | 231 | impl Rule for Date { 232 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 233 | if let Some(s) = value.downcast_ref::() { 234 | validate_date(s, &self.format, &self.min, &self.max) 235 | } else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 236 | validate_date(s, &self.format, &self.min, &self.max) 237 | } else if let Some(date) = (value as &dyn std::any::Any).downcast_ref::() { 238 | validate_naive_date(date, &self.min, &self.max) 239 | } else { 240 | Err(ValidationError::new("Value must be a string or date")) 241 | } 242 | } 243 | } 244 | 245 | fn validate_date( 246 | date_str: &str, 247 | format: &str, 248 | min: &Option, 249 | max: &Option 250 | ) -> Result<(), ValidationError> { 251 | match NaiveDate::parse_from_str(date_str, format) { 252 | Ok(date) => validate_naive_date(&date, min, max), 253 | Err(_) => Err(ValidationError::new(format!("Invalid date format, expected {}", format))), 254 | } 255 | } 256 | 257 | fn validate_naive_date( 258 | date: &NaiveDate, 259 | min: &Option, 260 | max: &Option 261 | ) -> Result<(), ValidationError> { 262 | if let Some(min_date) = min { 263 | if date < min_date { 264 | return Err(ValidationError::new(format!("Date must not be before {}", min_date))); 265 | } 266 | } 267 | 268 | if let Some(max_date) = max { 269 | if date > max_date { 270 | return Err(ValidationError::new(format!("Date must not be after {}", max_date))); 271 | } 272 | } 273 | 274 | Ok(()) 275 | } 276 | 277 | /// Custom validation rule using a closure 278 | pub struct Custom 279 | where 280 | F: for<'a> Fn(&'a dyn std::any::Any) -> Result<(), ValidationError> + Send + Sync, 281 | { 282 | pub validator: F, 283 | } 284 | 285 | impl Rule for Custom 286 | where 287 | F: for<'a> Fn(&'a dyn std::any::Any) -> Result<(), ValidationError> + Send + Sync, 288 | { 289 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 290 | (self.validator)(value) 291 | } 292 | } 293 | 294 | /// Phone number validation 295 | pub struct Phone { 296 | pub allow_empty: bool, 297 | } 298 | 299 | impl Rule for Phone { 300 | fn validate_any(&self, value: &dyn std::any::Any) -> Result<(), ValidationError> { 301 | if let Some(s) = value.downcast_ref::() { 302 | validate_phone(s, self.allow_empty) 303 | } else if let Some(s) = (value as &dyn std::any::Any).downcast_ref::<&str>() { 304 | validate_phone(s, self.allow_empty) 305 | } else { 306 | Err(ValidationError::new("Value must be a string")) 307 | } 308 | } 309 | } 310 | 311 | fn validate_phone(phone: &str, allow_empty: bool) -> Result<(), ValidationError> { 312 | if phone.is_empty() && allow_empty { 313 | return Ok(()); 314 | } 315 | 316 | // Basic phone validation: +1234567890 or 1234567890 317 | let phone_regex = Regex::new(r"^\+?\d{10,15}$").unwrap(); 318 | 319 | if !phone_regex.is_match(phone) { 320 | return Err(ValidationError::new("Invalid phone number format")); 321 | } 322 | 323 | Ok(()) 324 | } 325 | -------------------------------------------------------------------------------- /rustvalidity-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{quote, format_ident}; 3 | use syn::{parse_macro_input, DeriveInput, Data, Fields, Lit, Meta, NestedMeta, MetaNameValue}; 4 | 5 | /// Derive macro for implementing the Validate trait 6 | /// 7 | /// # Example 8 | /// 9 | /// ```rust 10 | /// #[derive(Validate)] 11 | /// struct User { 12 | /// #[validate(required, length(min = 3, max = 20))] 13 | /// username: String, 14 | /// 15 | /// #[validate(required, email)] 16 | /// email: String, 17 | /// 18 | /// #[validate(min = 18)] 19 | /// age: i32, 20 | /// } 21 | /// ``` 22 | #[proc_macro_derive(Validate, attributes(validate))] 23 | pub fn derive_validate(input: TokenStream) -> TokenStream { 24 | let input = parse_macro_input!(input as DeriveInput); 25 | 26 | // Get the name of the struct 27 | let name = &input.ident; 28 | 29 | // Get the fields of the struct 30 | let fields = match &input.data { 31 | Data::Struct(data) => { 32 | match &data.fields { 33 | Fields::Named(fields) => &fields.named, 34 | _ => panic!("Validate derive only supports structs with named fields"), 35 | } 36 | }, 37 | _ => panic!("Validate derive only supports structs"), 38 | }; 39 | 40 | // Generate validation code for each field 41 | let field_validations = fields.iter().map(|field| { 42 | let field_name = &field.ident; 43 | let field_name_str = field_name.as_ref().unwrap().to_string(); 44 | 45 | // Get validation attributes 46 | let validations = field.attrs.iter() 47 | .filter(|attr| attr.path.is_ident("validate")) 48 | .flat_map(|attr| { 49 | match attr.parse_meta() { 50 | Ok(Meta::List(meta_list)) => meta_list.nested, 51 | _ => panic!("Invalid validate attribute"), 52 | } 53 | }); 54 | 55 | // Generate validation code for each attribute 56 | let validation_code = validations.map(|validation| { 57 | match validation { 58 | NestedMeta::Meta(Meta::Path(path)) => { 59 | let rule_name = path.get_ident().unwrap().to_string(); 60 | let rule_ident = format_ident!("{}", rule_name); 61 | 62 | match rule_name.as_str() { 63 | "required" => quote! { 64 | if let Err(err) = validator.get_rule("required").unwrap().validate(&self.#field_name as &dyn Any) { 65 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 66 | } 67 | }, 68 | "email" => quote! { 69 | if let Err(err) = validator.get_rule("email").unwrap().validate(&self.#field_name as &dyn Any) { 70 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 71 | } 72 | }, 73 | "url" => quote! { 74 | if let Err(err) = validator.get_rule("url").unwrap().validate(&self.#field_name as &dyn Any) { 75 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 76 | } 77 | }, 78 | "uuid" => quote! { 79 | if let Err(err) = validator.get_rule("uuid").unwrap().validate(&self.#field_name as &dyn Any) { 80 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 81 | } 82 | }, 83 | "json" => quote! { 84 | if let Err(err) = validator.get_rule("json").unwrap().validate(&self.#field_name as &dyn Any) { 85 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 86 | } 87 | }, 88 | "positive" => quote! { 89 | if let Err(err) = validator.get_rule("positive").unwrap().validate(&self.#field_name as &dyn Any) { 90 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 91 | } 92 | }, 93 | "negative" => quote! { 94 | if let Err(err) = validator.get_rule("negative").unwrap().validate(&self.#field_name as &dyn Any) { 95 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 96 | } 97 | }, 98 | "unique" => quote! { 99 | if let Err(err) = validator.get_rule("unique").unwrap().validate(&self.#field_name as &dyn Any) { 100 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 101 | } 102 | }, 103 | "phone" => quote! { 104 | if let Err(err) = validator.get_rule("phone").unwrap().validate(&self.#field_name as &dyn Any) { 105 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 106 | } 107 | }, 108 | _ => quote! { 109 | // Custom rule 110 | if let Err(err) = validator.get_rule(#rule_name).unwrap().validate(&self.#field_name as &dyn Any) { 111 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 112 | } 113 | }, 114 | } 115 | }, 116 | NestedMeta::Meta(Meta::List(meta_list)) => { 117 | let rule_name = meta_list.path.get_ident().unwrap().to_string(); 118 | let rule_ident = format_ident!("{}", rule_name); 119 | 120 | match rule_name.as_str() { 121 | "length" => { 122 | let mut min = 0; 123 | let mut max = None; 124 | 125 | for nested in meta_list.nested.iter() { 126 | if let NestedMeta::Meta(Meta::NameValue(name_value)) = nested { 127 | let name = name_value.path.get_ident().unwrap().to_string(); 128 | if let Lit::Int(lit_int) = &name_value.lit { 129 | let value = lit_int.base10_parse::().unwrap(); 130 | if name == "min" { 131 | min = value; 132 | } else if name == "max" { 133 | max = Some(value); 134 | } 135 | } 136 | } 137 | } 138 | 139 | let rule_name = format!("{}_length", field_name_str); 140 | 141 | quote! { 142 | validator.add_rule(#rule_name, common::Length { min: #min, max: #max }); 143 | if let Err(err) = validator.get_rule(#rule_name).unwrap().validate(&self.#field_name as &dyn Any) { 144 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 145 | } 146 | } 147 | }, 148 | "min" => { 149 | let mut value = 0; 150 | 151 | for nested in meta_list.nested.iter() { 152 | if let NestedMeta::Lit(Lit::Int(lit_int)) = nested { 153 | value = lit_int.base10_parse::().unwrap(); 154 | } 155 | } 156 | 157 | let rule_name = format!("{}_min", field_name_str); 158 | 159 | quote! { 160 | validator.add_rule(#rule_name, numeric::Min { value: #value }); 161 | if let Err(err) = validator.get_rule(#rule_name).unwrap().validate(&self.#field_name as &dyn Any) { 162 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 163 | } 164 | } 165 | }, 166 | "max" => { 167 | let mut value = 0; 168 | 169 | for nested in meta_list.nested.iter() { 170 | if let NestedMeta::Lit(Lit::Int(lit_int)) = nested { 171 | value = lit_int.base10_parse::().unwrap(); 172 | } 173 | } 174 | 175 | let rule_name = format!("{}_max", field_name_str); 176 | 177 | quote! { 178 | validator.add_rule(#rule_name, numeric::Max { value: #value }); 179 | if let Err(err) = validator.get_rule(#rule_name).unwrap().validate(&self.#field_name as &dyn Any) { 180 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 181 | } 182 | } 183 | }, 184 | "range" => { 185 | let mut min = 0; 186 | let mut max = 0; 187 | 188 | for nested in meta_list.nested.iter() { 189 | if let NestedMeta::Meta(Meta::NameValue(name_value)) = nested { 190 | let name = name_value.path.get_ident().unwrap().to_string(); 191 | if let Lit::Int(lit_int) = &name_value.lit { 192 | let value = lit_int.base10_parse::().unwrap(); 193 | if name == "min" { 194 | min = value; 195 | } else if name == "max" { 196 | max = value; 197 | } 198 | } 199 | } 200 | } 201 | 202 | let rule_name = format!("{}_range", field_name_str); 203 | 204 | quote! { 205 | validator.add_rule(#rule_name, numeric::Range { min: #min, max: #max }); 206 | if let Err(err) = validator.get_rule(#rule_name).unwrap().validate(&self.#field_name as &dyn Any) { 207 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 208 | } 209 | } 210 | }, 211 | _ => quote! {}, 212 | } 213 | }, 214 | NestedMeta::Meta(Meta::NameValue(name_value)) => { 215 | let rule_name = name_value.path.get_ident().unwrap().to_string(); 216 | 217 | match rule_name.as_str() { 218 | "min" => { 219 | if let Lit::Int(lit_int) = &name_value.lit { 220 | let value = lit_int.base10_parse::().unwrap(); 221 | let rule_name = format!("{}_min", field_name_str); 222 | 223 | quote! { 224 | validator.add_rule(#rule_name, numeric::Min { value: #value }); 225 | if let Err(err) = validator.get_rule(#rule_name).unwrap().validate(&self.#field_name as &dyn Any) { 226 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 227 | } 228 | } 229 | } else { 230 | quote! {} 231 | } 232 | }, 233 | "max" => { 234 | if let Lit::Int(lit_int) = &name_value.lit { 235 | let value = lit_int.base10_parse::().unwrap(); 236 | let rule_name = format!("{}_max", field_name_str); 237 | 238 | quote! { 239 | validator.add_rule(#rule_name, numeric::Max { value: #value }); 240 | if let Err(err) = validator.get_rule(#rule_name).unwrap().validate(&self.#field_name as &dyn Any) { 241 | errors.entry(#field_name_str.to_string()).or_insert_with(Vec::new).push(format!("{}", err)); 242 | } 243 | } 244 | } else { 245 | quote! {} 246 | } 247 | }, 248 | _ => quote! {}, 249 | } 250 | }, 251 | _ => quote! {}, 252 | } 253 | }).collect::>(); 254 | 255 | quote! { 256 | #(#validation_code)* 257 | } 258 | }).collect::>(); 259 | 260 | // Generate the implementation of the Validate trait 261 | let expanded = quote! { 262 | impl Validate for #name { 263 | fn validate(&self) -> Result<(), ValidationError> { 264 | use std::any::Any; 265 | use std::collections::HashMap; 266 | 267 | // Create a new validator instance 268 | let mut validator = Validator::new(); 269 | 270 | // Add common validation rules 271 | validator.add_rule("required", common::Required); 272 | validator.add_rule("email", common::Email { check_dns: false }); 273 | validator.add_rule("url", common::Url { allowed_schemes: None }); 274 | validator.add_rule("uuid", common::Uuid); 275 | validator.add_rule("json", common::Json); 276 | validator.add_rule("positive", numeric::Positive); 277 | validator.add_rule("negative", numeric::Negative); 278 | validator.add_rule("unique", collection::Unique); 279 | validator.add_rule("phone", common::Phone { allow_empty: false }); 280 | 281 | // Validate fields 282 | let mut errors = HashMap::new(); 283 | 284 | #(#field_validations)* 285 | 286 | // Check if there are any validation errors 287 | if !errors.is_empty() { 288 | return Err(ValidationError::Multiple(errors)); 289 | } 290 | 291 | Ok(()) 292 | } 293 | } 294 | }; 295 | 296 | TokenStream::from(expanded) 297 | } 298 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "autocfg" 31 | version = "1.4.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 34 | 35 | [[package]] 36 | name = "bitflags" 37 | version = "2.9.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 40 | 41 | [[package]] 42 | name = "bumpalo" 43 | version = "3.17.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 46 | 47 | [[package]] 48 | name = "cc" 49 | version = "1.2.19" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" 52 | dependencies = [ 53 | "shlex", 54 | ] 55 | 56 | [[package]] 57 | name = "cfg-if" 58 | version = "1.0.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 61 | 62 | [[package]] 63 | name = "chrono" 64 | version = "0.4.40" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 67 | dependencies = [ 68 | "android-tzdata", 69 | "iana-time-zone", 70 | "js-sys", 71 | "num-traits", 72 | "wasm-bindgen", 73 | "windows-link", 74 | ] 75 | 76 | [[package]] 77 | name = "core-foundation-sys" 78 | version = "0.8.7" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 81 | 82 | [[package]] 83 | name = "displaydoc" 84 | version = "0.2.5" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 87 | dependencies = [ 88 | "proc-macro2", 89 | "quote", 90 | "syn", 91 | ] 92 | 93 | [[package]] 94 | name = "form_urlencoded" 95 | version = "1.2.1" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 98 | dependencies = [ 99 | "percent-encoding", 100 | ] 101 | 102 | [[package]] 103 | name = "getrandom" 104 | version = "0.3.2" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 107 | dependencies = [ 108 | "cfg-if", 109 | "libc", 110 | "r-efi", 111 | "wasi", 112 | ] 113 | 114 | [[package]] 115 | name = "iana-time-zone" 116 | version = "0.1.63" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 119 | dependencies = [ 120 | "android_system_properties", 121 | "core-foundation-sys", 122 | "iana-time-zone-haiku", 123 | "js-sys", 124 | "log", 125 | "wasm-bindgen", 126 | "windows-core", 127 | ] 128 | 129 | [[package]] 130 | name = "iana-time-zone-haiku" 131 | version = "0.1.2" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 134 | dependencies = [ 135 | "cc", 136 | ] 137 | 138 | [[package]] 139 | name = "icu_collections" 140 | version = "1.5.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 143 | dependencies = [ 144 | "displaydoc", 145 | "yoke", 146 | "zerofrom", 147 | "zerovec", 148 | ] 149 | 150 | [[package]] 151 | name = "icu_locid" 152 | version = "1.5.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 155 | dependencies = [ 156 | "displaydoc", 157 | "litemap", 158 | "tinystr", 159 | "writeable", 160 | "zerovec", 161 | ] 162 | 163 | [[package]] 164 | name = "icu_locid_transform" 165 | version = "1.5.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 168 | dependencies = [ 169 | "displaydoc", 170 | "icu_locid", 171 | "icu_locid_transform_data", 172 | "icu_provider", 173 | "tinystr", 174 | "zerovec", 175 | ] 176 | 177 | [[package]] 178 | name = "icu_locid_transform_data" 179 | version = "1.5.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 182 | 183 | [[package]] 184 | name = "icu_normalizer" 185 | version = "1.5.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 188 | dependencies = [ 189 | "displaydoc", 190 | "icu_collections", 191 | "icu_normalizer_data", 192 | "icu_properties", 193 | "icu_provider", 194 | "smallvec", 195 | "utf16_iter", 196 | "utf8_iter", 197 | "write16", 198 | "zerovec", 199 | ] 200 | 201 | [[package]] 202 | name = "icu_normalizer_data" 203 | version = "1.5.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 206 | 207 | [[package]] 208 | name = "icu_properties" 209 | version = "1.5.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 212 | dependencies = [ 213 | "displaydoc", 214 | "icu_collections", 215 | "icu_locid_transform", 216 | "icu_properties_data", 217 | "icu_provider", 218 | "tinystr", 219 | "zerovec", 220 | ] 221 | 222 | [[package]] 223 | name = "icu_properties_data" 224 | version = "1.5.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 227 | 228 | [[package]] 229 | name = "icu_provider" 230 | version = "1.5.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 233 | dependencies = [ 234 | "displaydoc", 235 | "icu_locid", 236 | "icu_provider_macros", 237 | "stable_deref_trait", 238 | "tinystr", 239 | "writeable", 240 | "yoke", 241 | "zerofrom", 242 | "zerovec", 243 | ] 244 | 245 | [[package]] 246 | name = "icu_provider_macros" 247 | version = "1.5.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 250 | dependencies = [ 251 | "proc-macro2", 252 | "quote", 253 | "syn", 254 | ] 255 | 256 | [[package]] 257 | name = "idna" 258 | version = "1.0.3" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 261 | dependencies = [ 262 | "idna_adapter", 263 | "smallvec", 264 | "utf8_iter", 265 | ] 266 | 267 | [[package]] 268 | name = "idna_adapter" 269 | version = "1.2.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 272 | dependencies = [ 273 | "icu_normalizer", 274 | "icu_properties", 275 | ] 276 | 277 | [[package]] 278 | name = "itoa" 279 | version = "1.0.15" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 282 | 283 | [[package]] 284 | name = "js-sys" 285 | version = "0.3.77" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 288 | dependencies = [ 289 | "once_cell", 290 | "wasm-bindgen", 291 | ] 292 | 293 | [[package]] 294 | name = "libc" 295 | version = "0.2.172" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 298 | 299 | [[package]] 300 | name = "litemap" 301 | version = "0.7.5" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 304 | 305 | [[package]] 306 | name = "log" 307 | version = "0.4.27" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 310 | 311 | [[package]] 312 | name = "memchr" 313 | version = "2.7.4" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 316 | 317 | [[package]] 318 | name = "num-traits" 319 | version = "0.2.19" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 322 | dependencies = [ 323 | "autocfg", 324 | ] 325 | 326 | [[package]] 327 | name = "once_cell" 328 | version = "1.21.3" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 331 | 332 | [[package]] 333 | name = "percent-encoding" 334 | version = "2.3.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 337 | 338 | [[package]] 339 | name = "proc-macro2" 340 | version = "1.0.95" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 343 | dependencies = [ 344 | "unicode-ident", 345 | ] 346 | 347 | [[package]] 348 | name = "quote" 349 | version = "1.0.40" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 352 | dependencies = [ 353 | "proc-macro2", 354 | ] 355 | 356 | [[package]] 357 | name = "r-efi" 358 | version = "5.2.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 361 | 362 | [[package]] 363 | name = "regex" 364 | version = "1.11.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 367 | dependencies = [ 368 | "aho-corasick", 369 | "memchr", 370 | "regex-automata", 371 | "regex-syntax", 372 | ] 373 | 374 | [[package]] 375 | name = "regex-automata" 376 | version = "0.4.9" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 379 | dependencies = [ 380 | "aho-corasick", 381 | "memchr", 382 | "regex-syntax", 383 | ] 384 | 385 | [[package]] 386 | name = "regex-syntax" 387 | version = "0.8.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 390 | 391 | [[package]] 392 | name = "rustvalidity" 393 | version = "0.1.0" 394 | dependencies = [ 395 | "chrono", 396 | "regex", 397 | "rustvalidity-derive", 398 | "serde", 399 | "serde_json", 400 | "thiserror", 401 | "url", 402 | "uuid", 403 | ] 404 | 405 | [[package]] 406 | name = "rustvalidity-derive" 407 | version = "0.1.0" 408 | dependencies = [ 409 | "proc-macro2", 410 | "quote", 411 | "syn", 412 | ] 413 | 414 | [[package]] 415 | name = "rustversion" 416 | version = "1.0.20" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 419 | 420 | [[package]] 421 | name = "ryu" 422 | version = "1.0.20" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 425 | 426 | [[package]] 427 | name = "serde" 428 | version = "1.0.219" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 431 | dependencies = [ 432 | "serde_derive", 433 | ] 434 | 435 | [[package]] 436 | name = "serde_derive" 437 | version = "1.0.219" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 440 | dependencies = [ 441 | "proc-macro2", 442 | "quote", 443 | "syn", 444 | ] 445 | 446 | [[package]] 447 | name = "serde_json" 448 | version = "1.0.140" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 451 | dependencies = [ 452 | "itoa", 453 | "memchr", 454 | "ryu", 455 | "serde", 456 | ] 457 | 458 | [[package]] 459 | name = "shlex" 460 | version = "1.3.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 463 | 464 | [[package]] 465 | name = "smallvec" 466 | version = "1.15.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 469 | 470 | [[package]] 471 | name = "stable_deref_trait" 472 | version = "1.2.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 475 | 476 | [[package]] 477 | name = "syn" 478 | version = "2.0.100" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 481 | dependencies = [ 482 | "proc-macro2", 483 | "quote", 484 | "unicode-ident", 485 | ] 486 | 487 | [[package]] 488 | name = "synstructure" 489 | version = "0.13.1" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 492 | dependencies = [ 493 | "proc-macro2", 494 | "quote", 495 | "syn", 496 | ] 497 | 498 | [[package]] 499 | name = "thiserror" 500 | version = "1.0.69" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 503 | dependencies = [ 504 | "thiserror-impl", 505 | ] 506 | 507 | [[package]] 508 | name = "thiserror-impl" 509 | version = "1.0.69" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 512 | dependencies = [ 513 | "proc-macro2", 514 | "quote", 515 | "syn", 516 | ] 517 | 518 | [[package]] 519 | name = "tinystr" 520 | version = "0.7.6" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 523 | dependencies = [ 524 | "displaydoc", 525 | "zerovec", 526 | ] 527 | 528 | [[package]] 529 | name = "unicode-ident" 530 | version = "1.0.18" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 533 | 534 | [[package]] 535 | name = "url" 536 | version = "2.5.4" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 539 | dependencies = [ 540 | "form_urlencoded", 541 | "idna", 542 | "percent-encoding", 543 | ] 544 | 545 | [[package]] 546 | name = "utf16_iter" 547 | version = "1.0.5" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 550 | 551 | [[package]] 552 | name = "utf8_iter" 553 | version = "1.0.4" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 556 | 557 | [[package]] 558 | name = "uuid" 559 | version = "1.16.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 562 | dependencies = [ 563 | "getrandom", 564 | ] 565 | 566 | [[package]] 567 | name = "wasi" 568 | version = "0.14.2+wasi-0.2.4" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 571 | dependencies = [ 572 | "wit-bindgen-rt", 573 | ] 574 | 575 | [[package]] 576 | name = "wasm-bindgen" 577 | version = "0.2.100" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 580 | dependencies = [ 581 | "cfg-if", 582 | "once_cell", 583 | "rustversion", 584 | "wasm-bindgen-macro", 585 | ] 586 | 587 | [[package]] 588 | name = "wasm-bindgen-backend" 589 | version = "0.2.100" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 592 | dependencies = [ 593 | "bumpalo", 594 | "log", 595 | "proc-macro2", 596 | "quote", 597 | "syn", 598 | "wasm-bindgen-shared", 599 | ] 600 | 601 | [[package]] 602 | name = "wasm-bindgen-macro" 603 | version = "0.2.100" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 606 | dependencies = [ 607 | "quote", 608 | "wasm-bindgen-macro-support", 609 | ] 610 | 611 | [[package]] 612 | name = "wasm-bindgen-macro-support" 613 | version = "0.2.100" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 616 | dependencies = [ 617 | "proc-macro2", 618 | "quote", 619 | "syn", 620 | "wasm-bindgen-backend", 621 | "wasm-bindgen-shared", 622 | ] 623 | 624 | [[package]] 625 | name = "wasm-bindgen-shared" 626 | version = "0.2.100" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 629 | dependencies = [ 630 | "unicode-ident", 631 | ] 632 | 633 | [[package]] 634 | name = "windows-core" 635 | version = "0.61.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 638 | dependencies = [ 639 | "windows-implement", 640 | "windows-interface", 641 | "windows-link", 642 | "windows-result", 643 | "windows-strings", 644 | ] 645 | 646 | [[package]] 647 | name = "windows-implement" 648 | version = "0.60.0" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 651 | dependencies = [ 652 | "proc-macro2", 653 | "quote", 654 | "syn", 655 | ] 656 | 657 | [[package]] 658 | name = "windows-interface" 659 | version = "0.59.1" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 662 | dependencies = [ 663 | "proc-macro2", 664 | "quote", 665 | "syn", 666 | ] 667 | 668 | [[package]] 669 | name = "windows-link" 670 | version = "0.1.1" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 673 | 674 | [[package]] 675 | name = "windows-result" 676 | version = "0.3.2" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 679 | dependencies = [ 680 | "windows-link", 681 | ] 682 | 683 | [[package]] 684 | name = "windows-strings" 685 | version = "0.4.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 688 | dependencies = [ 689 | "windows-link", 690 | ] 691 | 692 | [[package]] 693 | name = "wit-bindgen-rt" 694 | version = "0.39.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 697 | dependencies = [ 698 | "bitflags", 699 | ] 700 | 701 | [[package]] 702 | name = "write16" 703 | version = "1.0.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 706 | 707 | [[package]] 708 | name = "writeable" 709 | version = "0.5.5" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 712 | 713 | [[package]] 714 | name = "yoke" 715 | version = "0.7.5" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 718 | dependencies = [ 719 | "serde", 720 | "stable_deref_trait", 721 | "yoke-derive", 722 | "zerofrom", 723 | ] 724 | 725 | [[package]] 726 | name = "yoke-derive" 727 | version = "0.7.5" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 730 | dependencies = [ 731 | "proc-macro2", 732 | "quote", 733 | "syn", 734 | "synstructure", 735 | ] 736 | 737 | [[package]] 738 | name = "zerofrom" 739 | version = "0.1.6" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 742 | dependencies = [ 743 | "zerofrom-derive", 744 | ] 745 | 746 | [[package]] 747 | name = "zerofrom-derive" 748 | version = "0.1.6" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 751 | dependencies = [ 752 | "proc-macro2", 753 | "quote", 754 | "syn", 755 | "synstructure", 756 | ] 757 | 758 | [[package]] 759 | name = "zerovec" 760 | version = "0.10.4" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 763 | dependencies = [ 764 | "yoke", 765 | "zerofrom", 766 | "zerovec-derive", 767 | ] 768 | 769 | [[package]] 770 | name = "zerovec-derive" 771 | version = "0.10.3" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 774 | dependencies = [ 775 | "proc-macro2", 776 | "quote", 777 | "syn", 778 | ] 779 | --------------------------------------------------------------------------------