1use crate::config::ValidationRule;
4use crate::error::AppError;
5use regex::Regex;
6use serde_json::Value;
7use std::collections::HashMap;
8
9pub struct RequestValidator;
10
11impl RequestValidator {
12 pub fn validate(
14 body: &HashMap<String, Value>,
15 rules: &HashMap<String, ValidationRule>,
16 ) -> Result<(), AppError> {
17 for (col, rule) in rules {
18 let val = body.get(col);
19 if rule.required == Some(true) && (val.is_none() || val == Some(&Value::Null)) {
20 return Err(AppError::Validation(format!("{} is required", col)));
21 }
22 if let Some(v) = val {
23 validate_field(col, v, rule)?;
24 }
25 }
26 Ok(())
27 }
28
29 pub fn validate_partial(
31 body: &HashMap<String, Value>,
32 rules: &HashMap<String, ValidationRule>,
33 ) -> Result<(), AppError> {
34 for (col, v) in body {
35 if let Some(rule) = rules.get(col) {
36 validate_field(col, v, rule)?;
37 }
38 }
39 Ok(())
40 }
41
42 pub fn validate_collecting(
45 body: &HashMap<String, Value>,
46 rules: &HashMap<String, ValidationRule>,
47 ) -> Vec<(String, String)> {
48 let mut errors = Vec::new();
49 for (col, rule) in rules {
50 let val = body.get(col);
51 if rule.required == Some(true) && (val.is_none() || val == Some(&Value::Null)) {
52 errors.push((col.clone(), format!("{} is required", col)));
53 continue;
54 }
55 if let Some(v) = val {
56 if let Err(AppError::Validation(msg)) = validate_field(col, v, rule) {
57 errors.push((col.clone(), msg));
58 }
59 }
60 }
61 errors
62 }
63}
64
65fn validate_field(col: &str, v: &Value, rule: &ValidationRule) -> Result<(), AppError> {
66 if v.is_null() {
67 return Ok(());
68 }
69 if let Some(format) = &rule.format {
70 validate_format(col, v, format)?;
71 }
72 if let Some(max) = rule.max_length {
73 if let Some(s) = v.as_str() {
74 if s.len() > max as usize {
75 return Err(AppError::Validation(format!(
76 "{} must be at most {} characters",
77 col, max
78 )));
79 }
80 }
81 }
82 if let Some(min) = rule.min_length {
83 if let Some(s) = v.as_str() {
84 if s.len() < min as usize {
85 return Err(AppError::Validation(format!(
86 "{} must be at least {} characters",
87 col, min
88 )));
89 }
90 }
91 }
92 if let Some(ref pattern) = rule.pattern {
93 let re = Regex::new(pattern)
94 .map_err(|_| AppError::Validation(format!("invalid pattern for {}", col)))?;
95 if let Some(s) = v.as_str() {
96 if !re.is_match(s) {
97 return Err(AppError::Validation(format!(
98 "{} does not match required pattern",
99 col
100 )));
101 }
102 }
103 }
104 if let Some(ref allowed) = rule.allowed {
105 let mut ok = false;
106 for a in allowed {
107 if value_eq(v, a) {
108 ok = true;
109 break;
110 }
111 }
112 if !ok {
113 return Err(AppError::Validation(format!(
114 "{} must be one of: {:?}",
115 col,
116 allowed.iter().take(5).collect::<Vec<_>>()
117 )));
118 }
119 }
120 if let Some(min) = rule.minimum {
121 if let Some(n) = v.as_f64() {
122 if n < min {
123 return Err(AppError::Validation(format!(
124 "{} must be at least {}",
125 col, min
126 )));
127 }
128 }
129 }
130 if let Some(max) = rule.maximum {
131 if let Some(n) = v.as_f64() {
132 if n > max {
133 return Err(AppError::Validation(format!(
134 "{} must be at most {}",
135 col, max
136 )));
137 }
138 }
139 }
140 Ok(())
141}
142
143fn value_eq(a: &Value, b: &Value) -> bool {
144 match (a, b) {
145 (Value::String(s), Value::String(t)) => s == t,
146 (Value::Number(n), Value::Number(m)) => n.as_f64() == m.as_f64(),
147 _ => a == b,
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::config::ValidationRule;
155 use serde_json::json;
156
157 fn rule(f: impl FnOnce(&mut ValidationRule)) -> ValidationRule {
158 let mut r = ValidationRule::default();
159 f(&mut r);
160 r
161 }
162
163 fn body(pairs: &[(&str, serde_json::Value)]) -> HashMap<String, Value> {
164 pairs
165 .iter()
166 .map(|(k, v)| (k.to_string(), v.clone()))
167 .collect()
168 }
169
170 fn rules_map(pairs: &[(&str, ValidationRule)]) -> HashMap<String, ValidationRule> {
171 pairs
172 .iter()
173 .map(|(k, v)| (k.to_string(), v.clone()))
174 .collect()
175 }
176
177 #[test]
180 fn required_field_present_passes() {
181 let rules = rules_map(&[("name", rule(|r| r.required = Some(true)))]);
182 let b = body(&[("name", json!("Alice"))]);
183 assert!(RequestValidator::validate(&b, &rules).is_ok());
184 }
185
186 #[test]
187 fn required_field_missing_fails() {
188 let rules = rules_map(&[("name", rule(|r| r.required = Some(true)))]);
189 let b = body(&[]);
190 assert!(RequestValidator::validate(&b, &rules).is_err());
191 }
192
193 #[test]
194 fn required_field_null_fails() {
195 let rules = rules_map(&[("name", rule(|r| r.required = Some(true)))]);
196 let b = body(&[("name", json!(null))]);
197 assert!(RequestValidator::validate(&b, &rules).is_err());
198 }
199
200 #[test]
201 fn optional_field_absent_passes() {
202 let rules = rules_map(&[("bio", rule(|_| {}))]);
203 let b = body(&[]);
204 assert!(RequestValidator::validate(&b, &rules).is_ok());
205 }
206
207 #[test]
210 fn partial_skips_missing_required() {
211 let rules = rules_map(&[("name", rule(|r| r.required = Some(true)))]);
212 let b = body(&[]); assert!(RequestValidator::validate_partial(&b, &rules).is_ok());
214 }
215
216 #[test]
217 fn partial_validates_present_field() {
218 let rules = rules_map(&[("email", rule(|r| r.format = Some("email".into())))]);
219 let b = body(&[("email", json!("not-an-email"))]);
220 assert!(RequestValidator::validate_partial(&b, &rules).is_err());
221 }
222
223 #[test]
226 fn email_valid() {
227 let rules = rules_map(&[("email", rule(|r| r.format = Some("email".into())))]);
228 let b = body(&[("email", json!("user@example.com"))]);
229 assert!(RequestValidator::validate(&b, &rules).is_ok());
230 }
231
232 #[test]
233 fn email_invalid_no_at() {
234 let rules = rules_map(&[("email", rule(|r| r.format = Some("email".into())))]);
235 let b = body(&[("email", json!("notanemail"))]);
236 assert!(RequestValidator::validate(&b, &rules).is_err());
237 }
238
239 #[test]
242 fn uuid_valid() {
243 let rules = rules_map(&[("id", rule(|r| r.format = Some("uuid".into())))]);
244 let b = body(&[("id", json!("550e8400-e29b-41d4-a716-446655440000"))]);
245 assert!(RequestValidator::validate(&b, &rules).is_ok());
246 }
247
248 #[test]
249 fn uuid_invalid() {
250 let rules = rules_map(&[("id", rule(|r| r.format = Some("uuid".into())))]);
251 let b = body(&[("id", json!("not-a-uuid"))]);
252 assert!(RequestValidator::validate(&b, &rules).is_err());
253 }
254
255 #[test]
258 fn max_length_pass() {
259 let rules = rules_map(&[("bio", rule(|r| r.max_length = Some(10)))]);
260 let b = body(&[("bio", json!("hello"))]);
261 assert!(RequestValidator::validate(&b, &rules).is_ok());
262 }
263
264 #[test]
265 fn max_length_fail() {
266 let rules = rules_map(&[("bio", rule(|r| r.max_length = Some(3)))]);
267 let b = body(&[("bio", json!("toolong"))]);
268 assert!(RequestValidator::validate(&b, &rules).is_err());
269 }
270
271 #[test]
272 fn min_length_pass() {
273 let rules = rules_map(&[("code", rule(|r| r.min_length = Some(3)))]);
274 let b = body(&[("code", json!("abc"))]);
275 assert!(RequestValidator::validate(&b, &rules).is_ok());
276 }
277
278 #[test]
279 fn min_length_fail() {
280 let rules = rules_map(&[("code", rule(|r| r.min_length = Some(5)))]);
281 let b = body(&[("code", json!("hi"))]);
282 assert!(RequestValidator::validate(&b, &rules).is_err());
283 }
284
285 #[test]
288 fn pattern_match_passes() {
289 let rules = rules_map(&[("zip", rule(|r| r.pattern = Some(r"^\d{5}$".into())))]);
290 let b = body(&[("zip", json!("12345"))]);
291 assert!(RequestValidator::validate(&b, &rules).is_ok());
292 }
293
294 #[test]
295 fn pattern_no_match_fails() {
296 let rules = rules_map(&[("zip", rule(|r| r.pattern = Some(r"^\d{5}$".into())))]);
297 let b = body(&[("zip", json!("abc"))]);
298 assert!(RequestValidator::validate(&b, &rules).is_err());
299 }
300
301 #[test]
304 fn allowed_values_pass() {
305 let rules = rules_map(&[(
306 "status",
307 rule(|r| r.allowed = Some(vec![json!("active"), json!("inactive")])),
308 )]);
309 let b = body(&[("status", json!("active"))]);
310 assert!(RequestValidator::validate(&b, &rules).is_ok());
311 }
312
313 #[test]
314 fn allowed_values_fail() {
315 let rules = rules_map(&[(
316 "status",
317 rule(|r| r.allowed = Some(vec![json!("active"), json!("inactive")])),
318 )]);
319 let b = body(&[("status", json!("pending"))]);
320 assert!(RequestValidator::validate(&b, &rules).is_err());
321 }
322
323 #[test]
326 fn minimum_passes() {
327 let rules = rules_map(&[("age", rule(|r| r.minimum = Some(0.0)))]);
328 let b = body(&[("age", json!(5))]);
329 assert!(RequestValidator::validate(&b, &rules).is_ok());
330 }
331
332 #[test]
333 fn minimum_fails() {
334 let rules = rules_map(&[("age", rule(|r| r.minimum = Some(18.0)))]);
335 let b = body(&[("age", json!(10))]);
336 assert!(RequestValidator::validate(&b, &rules).is_err());
337 }
338
339 #[test]
340 fn maximum_passes() {
341 let rules = rules_map(&[("score", rule(|r| r.maximum = Some(100.0)))]);
342 let b = body(&[("score", json!(99))]);
343 assert!(RequestValidator::validate(&b, &rules).is_ok());
344 }
345
346 #[test]
347 fn maximum_fails() {
348 let rules = rules_map(&[("score", rule(|r| r.maximum = Some(100.0)))]);
349 let b = body(&[("score", json!(101))]);
350 assert!(RequestValidator::validate(&b, &rules).is_err());
351 }
352
353 #[test]
356 fn null_value_skips_format_check() {
357 let rules = rules_map(&[("email", rule(|r| r.format = Some("email".into())))]);
358 let b = body(&[("email", json!(null))]);
359 assert!(RequestValidator::validate(&b, &rules).is_ok());
361 }
362
363 #[test]
366 fn collecting_returns_all_errors() {
367 let rules = rules_map(&[
368 ("name", rule(|r| r.required = Some(true))),
369 ("email", rule(|r| r.required = Some(true))),
370 ]);
371 let b = body(&[]);
372 let errors = RequestValidator::validate_collecting(&b, &rules);
373 assert_eq!(errors.len(), 2);
374 }
375
376 #[test]
377 fn collecting_returns_empty_on_success() {
378 let rules = rules_map(&[("name", rule(|r| r.required = Some(true)))]);
379 let b = body(&[("name", json!("Alice"))]);
380 let errors = RequestValidator::validate_collecting(&b, &rules);
381 assert!(errors.is_empty());
382 }
383}
384
385fn validate_format(col: &str, v: &Value, format: &str) -> Result<(), AppError> {
386 match format.to_lowercase().as_str() {
387 "email" => {
388 if let Some(s) = v.as_str() {
389 if !s.contains('@') || s.len() < 3 {
390 return Err(AppError::Validation(format!(
391 "{} must be a valid email",
392 col
393 )));
394 }
395 }
396 }
397 "uuid" => {
398 if let Some(s) = v.as_str() {
399 if uuid::Uuid::parse_str(s).is_err() {
400 return Err(AppError::Validation(format!(
401 "{} must be a valid UUID",
402 col
403 )));
404 }
405 }
406 }
407 _ => {}
408 }
409 Ok(())
410}