elif_validation/validators/
numeric.rs

1//! Numeric value validators
2
3use crate::error::{ValidationError, ValidationResult};
4use crate::traits::ValidationRule;
5use async_trait::async_trait;
6use serde_json::Value;
7
8/// Validator for numeric constraints
9#[derive(Debug, Clone)]
10pub struct NumericValidator {
11    /// Minimum value (inclusive)
12    pub min: Option<f64>,
13    /// Maximum value (inclusive)
14    pub max: Option<f64>,
15    /// Allow only integers (no decimals)
16    pub integer_only: bool,
17    /// Allow only positive numbers (> 0)
18    pub positive_only: bool,
19    /// Allow only negative numbers (< 0)
20    pub negative_only: bool,
21    /// Custom error message
22    pub message: Option<String>,
23}
24
25impl NumericValidator {
26    /// Create a new numeric validator with default settings
27    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    /// Set minimum value constraint
39    pub fn min(mut self, min: f64) -> Self {
40        self.min = Some(min);
41        self
42    }
43
44    /// Set maximum value constraint
45    pub fn max(mut self, max: f64) -> Self {
46        self.max = Some(max);
47        self
48    }
49
50    /// Set value range (min and max)
51    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    /// Require integer values only (no decimals)
58    pub fn integer_only(mut self, integer_only: bool) -> Self {
59        self.integer_only = integer_only;
60        self
61    }
62
63    /// Allow only positive numbers (> 0)
64    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    /// Allow only negative numbers (< 0)
73    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    /// Set custom error message
82    pub fn message(mut self, message: impl Into<String>) -> Self {
83        self.message = Some(message.into());
84        self
85    }
86
87    /// Extract numeric value from JSON Value
88    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    /// Check if a number is an integer
97    fn is_integer(&self, num: f64) -> bool {
98        num.fract() == 0.0
99    }
100
101    /// Generate appropriate error message
102    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        // Skip validation for null values
149        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        // Check for NaN or infinity
165        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        // Check integer requirement
174        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        // Check positive/negative requirements
183        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        // Check minimum value
200        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        // Check maximum value
211        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        // Valid numbers
262        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        // String representations of numbers
267        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        // Invalid types
271        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        // Within range
280        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        // Out of range
285        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        // Valid integers
294        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        // Invalid decimals
299        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        // Valid positive numbers
308        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        // Invalid non-positive numbers
312        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        // Valid negative numbers
321        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        // Invalid non-negative numbers
325        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        // Valid: positive integer in range
337        assert!(validator.validate(&Value::Number(serde_json::Number::from(42)), "level").await.is_ok());
338        
339        // Invalid: decimal
340        assert!(validator.validate(&Value::Number(serde_json::Number::from_f64(42.5).unwrap()), "level").await.is_err());
341        
342        // Invalid: out of range
343        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        // Invalid: negative
347        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        // Valid string numbers
355        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        // Invalid string numbers (out of range)
360        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        // Invalid string (not a number)
364        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        // Test with infinity and NaN strings (should be rejected)
372        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        // Null values should be skipped
396        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        // Test below minimum error code
408        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        // Test not integer error code
414        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        // Test above maximum error code
420        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}