armature_validation/
rules.rs1use crate::ValidationError;
4use std::sync::Arc;
5
6type ValidatorFn = Arc<dyn Fn(&str, &str) -> Result<(), ValidationError> + Send + Sync>;
7
8#[derive(Clone)]
10pub struct ValidationRules {
11 validators: Vec<ValidatorFn>,
12 field: String,
13}
14
15impl ValidationRules {
16 pub fn for_field(field: impl Into<String>) -> Self {
18 Self {
19 validators: Vec::new(),
20 field: field.into(),
21 }
22 }
23
24 #[allow(clippy::should_implement_trait)]
26 pub fn add<F>(mut self, validator: F) -> Self
27 where
28 F: Fn(&str, &str) -> Result<(), ValidationError> + Send + Sync + 'static,
29 {
30 self.validators.push(Arc::new(validator));
31 self
32 }
33
34 pub fn validate(&self, value: &str) -> Result<(), Vec<ValidationError>> {
36 let mut errors = Vec::new();
37
38 for validator in &self.validators {
39 if let Err(error) = validator(value, &self.field) {
40 errors.push(error);
41 }
42 }
43
44 if errors.is_empty() {
45 Ok(())
46 } else {
47 Err(errors)
48 }
49 }
50}
51
52pub struct ValidationBuilder {
54 rules: Vec<ValidationRules>,
55}
56
57impl ValidationBuilder {
58 pub fn new() -> Self {
60 Self { rules: Vec::new() }
61 }
62
63 pub fn field(mut self, rules: ValidationRules) -> Self {
65 self.rules.push(rules);
66 self
67 }
68
69 pub fn validate(
71 &self,
72 data: &std::collections::HashMap<String, String>,
73 ) -> Result<(), Vec<ValidationError>> {
74 let mut all_errors = Vec::new();
75
76 for rule in &self.rules {
77 if let Some(value) = data.get(&rule.field) {
78 if let Err(mut errors) = rule.validate(value) {
79 all_errors.append(&mut errors);
80 }
81 }
82 }
83
84 if all_errors.is_empty() {
85 Ok(())
86 } else {
87 Err(all_errors)
88 }
89 }
90
91 pub async fn validate_parallel(
124 &self,
125 data: &std::collections::HashMap<String, String>,
126 ) -> Result<(), Vec<ValidationError>> {
127 use tokio::task::JoinSet;
128
129 let mut set = JoinSet::new();
130
131 for rule in &self.rules {
133 if let Some(value) = data.get(&rule.field) {
134 let value = value.clone();
135 let field = rule.field.clone();
136 let validators = rule.validators.clone();
137
138 set.spawn(async move {
139 let mut errors = Vec::new();
140 for validator in &validators {
141 if let Err(error) = validator(&value, &field) {
142 errors.push(error);
143 }
144 }
145 errors
146 });
147 }
148 }
149
150 let mut all_errors = Vec::new();
152 while let Some(result) = set.join_next().await {
153 match result {
154 Ok(mut errors) => all_errors.append(&mut errors),
155 Err(e) => {
156 return Err(vec![ValidationError {
157 field: "unknown".to_string(),
158 message: format!("Validation task failed: {}", e),
159 constraint: "task_error".to_string(),
160 value: None,
161 }]);
162 }
163 }
164 }
165
166 if all_errors.is_empty() {
167 Ok(())
168 } else {
169 Err(all_errors)
170 }
171 }
172}
173
174impl Default for ValidationBuilder {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::validators::*;
184
185 #[test]
186 fn test_validation_rules() {
187 let rules = ValidationRules::for_field("email")
188 .add(NotEmpty::validate)
189 .add(IsEmail::validate);
190
191 assert!(rules.validate("test@example.com").is_ok());
192 assert!(rules.validate("invalid").is_err());
193 assert!(rules.validate("").is_err());
194 }
195
196 #[test]
197 fn test_validation_builder() {
198 let mut data = std::collections::HashMap::new();
199 data.insert("name".to_string(), "John".to_string());
200 data.insert("email".to_string(), "john@example.com".to_string());
201
202 let builder = ValidationBuilder::new()
203 .field(ValidationRules::for_field("name").add(NotEmpty::validate))
204 .field(ValidationRules::for_field("email").add(IsEmail::validate));
205
206 assert!(builder.validate(&data).is_ok());
207 }
208}