elif_validation/validators/
numeric.rs1use crate::error::{ValidationError, ValidationResult};
4use crate::traits::ValidationRule;
5use async_trait::async_trait;
6use serde_json::Value;
7
8#[derive(Debug, Clone)]
10pub struct NumericValidator {
11 pub min: Option<f64>,
13 pub max: Option<f64>,
15 pub integer_only: bool,
17 pub positive_only: bool,
19 pub negative_only: bool,
21 pub message: Option<String>,
23}
24
25impl NumericValidator {
26 pub fn new() -> Self {
28 Self {
29 min: None,
30 max: None,
31 integer_only: false,
32 positive_only: false,
33 negative_only: false,
34 message: None,
35 }
36 }
37
38 pub fn min(mut self, min: f64) -> Self {
40 self.min = Some(min);
41 self
42 }
43
44 pub fn max(mut self, max: f64) -> Self {
46 self.max = Some(max);
47 self
48 }
49
50 pub fn range(mut self, min: f64, max: f64) -> Self {
52 self.min = Some(min);
53 self.max = Some(max);
54 self
55 }
56
57 pub fn integer_only(mut self, integer_only: bool) -> Self {
59 self.integer_only = integer_only;
60 self
61 }
62
63 pub fn positive_only(mut self, positive_only: bool) -> Self {
65 self.positive_only = positive_only;
66 if positive_only {
67 self.negative_only = false;
68 }
69 self
70 }
71
72 pub fn negative_only(mut self, negative_only: bool) -> Self {
74 self.negative_only = negative_only;
75 if negative_only {
76 self.positive_only = false;
77 }
78 self
79 }
80
81 pub fn message(mut self, message: impl Into<String>) -> Self {
83 self.message = Some(message.into());
84 self
85 }
86
87 fn get_numeric_value(&self, value: &Value) -> Option<f64> {
89 match value {
90 Value::Number(num) => num.as_f64(),
91 Value::String(s) => s.parse::<f64>().ok(),
92 _ => None,
93 }
94 }
95
96 fn is_integer(&self, num: f64) -> bool {
98 num.fract() == 0.0
99 }
100
101 fn create_error_message(&self, field: &str, value: f64) -> String {
103 if let Some(ref custom_message) = self.message {
104 return custom_message.clone();
105 }
106
107 if self.positive_only && value <= 0.0 {
108 return format!("{} must be a positive number", field);
109 }
110
111 if self.negative_only && value >= 0.0 {
112 return format!("{} must be a negative number", field);
113 }
114
115 if self.integer_only && !self.is_integer(value) {
116 return format!("{} must be an integer", field);
117 }
118
119 match (self.min, self.max) {
120 (Some(min), Some(max)) if min == max => {
121 format!("{} must equal {}", field, min)
122 }
123 (Some(min), Some(max)) => {
124 format!("{} must be between {} and {}", field, min, max)
125 }
126 (Some(min), None) => {
127 format!("{} must be at least {}", field, min)
128 }
129 (None, Some(max)) => {
130 format!("{} must be at most {}", field, max)
131 }
132 (None, None) => {
133 format!("{} has invalid numeric value: {}", field, value)
134 }
135 }
136 }
137}
138
139impl Default for NumericValidator {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145#[async_trait]
146impl ValidationRule for NumericValidator {
147 async fn validate(&self, value: &Value, field: &str) -> ValidationResult<()> {
148 if value.is_null() {
150 return Ok(());
151 }
152
153 let num = match self.get_numeric_value(value) {
154 Some(n) => n,
155 None => {
156 return Err(ValidationError::with_code(
157 field,
158 format!("{} must be a numeric value", field),
159 "invalid_type",
160 ).into());
161 }
162 };
163
164 if !num.is_finite() {
166 return Err(ValidationError::with_code(
167 field,
168 format!("{} must be a finite number", field),
169 "invalid_number",
170 ).into());
171 }
172
173 if self.integer_only && !self.is_integer(num) {
175 return Err(ValidationError::with_code(
176 field,
177 self.create_error_message(field, num),
178 "not_integer",
179 ).into());
180 }
181
182 if self.positive_only && num <= 0.0 {
184 return Err(ValidationError::with_code(
185 field,
186 self.create_error_message(field, num),
187 "not_positive",
188 ).into());
189 }
190
191 if self.negative_only && num >= 0.0 {
192 return Err(ValidationError::with_code(
193 field,
194 self.create_error_message(field, num),
195 "not_negative",
196 ).into());
197 }
198
199 if let Some(min) = self.min {
201 if num < min {
202 return Err(ValidationError::with_code(
203 field,
204 self.create_error_message(field, num),
205 "below_minimum",
206 ).into());
207 }
208 }
209
210 if let Some(max) = self.max {
212 if num > max {
213 return Err(ValidationError::with_code(
214 field,
215 self.create_error_message(field, num),
216 "above_maximum",
217 ).into());
218 }
219 }
220
221 Ok(())
222 }
223
224 fn rule_name(&self) -> &'static str {
225 "numeric"
226 }
227
228 fn parameters(&self) -> Option<Value> {
229 let mut params = serde_json::Map::new();
230
231 if let Some(min) = self.min {
232 params.insert("min".to_string(), Value::Number(
233 serde_json::Number::from_f64(min).unwrap_or(serde_json::Number::from(0))
234 ));
235 }
236 if let Some(max) = self.max {
237 params.insert("max".to_string(), Value::Number(
238 serde_json::Number::from_f64(max).unwrap_or(serde_json::Number::from(0))
239 ));
240 }
241 params.insert("integer_only".to_string(), Value::Bool(self.integer_only));
242 params.insert("positive_only".to_string(), Value::Bool(self.positive_only));
243 params.insert("negative_only".to_string(), Value::Bool(self.negative_only));
244
245 if let Some(ref message) = self.message {
246 params.insert("message".to_string(), Value::String(message.clone()));
247 }
248
249 Some(Value::Object(params))
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[tokio::test]
258 async fn test_numeric_validator_basic() {
259 let validator = NumericValidator::new();
260
261 assert!(validator.validate(&Value::Number(serde_json::Number::from(42)), "age").await.is_ok());
263 assert!(validator.validate(&Value::Number(serde_json::Number::from(-10)), "temp").await.is_ok());
264 assert!(validator.validate(&Value::Number(serde_json::Number::from_f64(3.14).unwrap()), "pi").await.is_ok());
265
266 assert!(validator.validate(&Value::String("42".to_string()), "age").await.is_ok());
268 assert!(validator.validate(&Value::String("3.14".to_string()), "pi").await.is_ok());
269
270 assert!(validator.validate(&Value::String("not-a-number".to_string()), "age").await.is_err());
272 assert!(validator.validate(&Value::Bool(true), "age").await.is_err());
273 }
274
275 #[tokio::test]
276 async fn test_numeric_validator_min_max() {
277 let validator = NumericValidator::new().range(0.0, 100.0);
278
279 assert!(validator.validate(&Value::Number(serde_json::Number::from(50)), "score").await.is_ok());
281 assert!(validator.validate(&Value::Number(serde_json::Number::from(0)), "score").await.is_ok());
282 assert!(validator.validate(&Value::Number(serde_json::Number::from(100)), "score").await.is_ok());
283
284 assert!(validator.validate(&Value::Number(serde_json::Number::from(-1)), "score").await.is_err());
286 assert!(validator.validate(&Value::Number(serde_json::Number::from(101)), "score").await.is_err());
287 }
288
289 #[tokio::test]
290 async fn test_numeric_validator_integer_only() {
291 let validator = NumericValidator::new().integer_only(true);
292
293 assert!(validator.validate(&Value::Number(serde_json::Number::from(42)), "count").await.is_ok());
295 assert!(validator.validate(&Value::Number(serde_json::Number::from(0)), "count").await.is_ok());
296 assert!(validator.validate(&Value::Number(serde_json::Number::from(-10)), "count").await.is_ok());
297
298 assert!(validator.validate(&Value::Number(serde_json::Number::from_f64(3.14).unwrap()), "count").await.is_err());
300 assert!(validator.validate(&Value::String("2.5".to_string()), "count").await.is_err());
301 }
302
303 #[tokio::test]
304 async fn test_numeric_validator_positive_only() {
305 let validator = NumericValidator::new().positive_only(true);
306
307 assert!(validator.validate(&Value::Number(serde_json::Number::from(1)), "amount").await.is_ok());
309 assert!(validator.validate(&Value::Number(serde_json::Number::from_f64(0.1).unwrap()), "amount").await.is_ok());
310
311 assert!(validator.validate(&Value::Number(serde_json::Number::from(0)), "amount").await.is_err());
313 assert!(validator.validate(&Value::Number(serde_json::Number::from(-1)), "amount").await.is_err());
314 }
315
316 #[tokio::test]
317 async fn test_numeric_validator_negative_only() {
318 let validator = NumericValidator::new().negative_only(true);
319
320 assert!(validator.validate(&Value::Number(serde_json::Number::from(-1)), "debt").await.is_ok());
322 assert!(validator.validate(&Value::Number(serde_json::Number::from_f64(-0.1).unwrap()), "debt").await.is_ok());
323
324 assert!(validator.validate(&Value::Number(serde_json::Number::from(0)), "debt").await.is_err());
326 assert!(validator.validate(&Value::Number(serde_json::Number::from(1)), "debt").await.is_err());
327 }
328
329 #[tokio::test]
330 async fn test_numeric_validator_combined_constraints() {
331 let validator = NumericValidator::new()
332 .range(1.0, 100.0)
333 .integer_only(true)
334 .positive_only(true);
335
336 assert!(validator.validate(&Value::Number(serde_json::Number::from(42)), "level").await.is_ok());
338
339 assert!(validator.validate(&Value::Number(serde_json::Number::from_f64(42.5).unwrap()), "level").await.is_err());
341
342 assert!(validator.validate(&Value::Number(serde_json::Number::from(0)), "level").await.is_err());
344 assert!(validator.validate(&Value::Number(serde_json::Number::from(101)), "level").await.is_err());
345
346 assert!(validator.validate(&Value::Number(serde_json::Number::from(-10)), "level").await.is_err());
348 }
349
350 #[tokio::test]
351 async fn test_numeric_validator_string_parsing() {
352 let validator = NumericValidator::new().range(0.0, 10.0);
353
354 assert!(validator.validate(&Value::String("5".to_string()), "rating").await.is_ok());
356 assert!(validator.validate(&Value::String("7.5".to_string()), "rating").await.is_ok());
357 assert!(validator.validate(&Value::String("0".to_string()), "rating").await.is_ok());
358
359 assert!(validator.validate(&Value::String("-1".to_string()), "rating").await.is_err());
361 assert!(validator.validate(&Value::String("11".to_string()), "rating").await.is_err());
362
363 assert!(validator.validate(&Value::String("not-a-number".to_string()), "rating").await.is_err());
365 }
366
367 #[tokio::test]
368 async fn test_numeric_validator_infinity_nan() {
369 let validator = NumericValidator::new();
370
371 assert!(validator.validate(&Value::String("inf".to_string()), "value").await.is_err());
373 assert!(validator.validate(&Value::String("infinity".to_string()), "value").await.is_err());
374 assert!(validator.validate(&Value::String("NaN".to_string()), "value").await.is_err());
375 }
376
377 #[tokio::test]
378 async fn test_numeric_validator_custom_message() {
379 let validator = NumericValidator::new()
380 .min(18.0)
381 .message("Must be at least 18 years old");
382
383 let result = validator.validate(&Value::Number(serde_json::Number::from(16)), "age").await;
384 assert!(result.is_err());
385
386 let errors = result.unwrap_err();
387 let field_errors = errors.get_field_errors("age").unwrap();
388 assert_eq!(field_errors[0].message, "Must be at least 18 years old");
389 }
390
391 #[tokio::test]
392 async fn test_numeric_validator_with_null() {
393 let validator = NumericValidator::new().min(0.0);
394
395 let result = validator.validate(&Value::Null, "optional_number").await;
397 assert!(result.is_ok());
398 }
399
400 #[tokio::test]
401 async fn test_numeric_validator_error_codes() {
402 let validator = NumericValidator::new()
403 .range(0.0, 100.0)
404 .integer_only(true)
405 .positive_only(true);
406
407 let result = validator.validate(&Value::Number(serde_json::Number::from(-1)), "value").await;
409 assert!(result.is_err());
410 let errors = result.unwrap_err();
411 assert_eq!(errors.errors["value"][0].code, "not_positive");
412
413 let result = validator.validate(&Value::Number(serde_json::Number::from_f64(1.5).unwrap()), "value").await;
415 assert!(result.is_err());
416 let errors = result.unwrap_err();
417 assert_eq!(errors.errors["value"][0].code, "not_integer");
418
419 let result = validator.validate(&Value::Number(serde_json::Number::from(101)), "value").await;
421 assert!(result.is_err());
422 let errors = result.unwrap_err();
423 assert_eq!(errors.errors["value"][0].code, "above_maximum");
424 }
425}