Skip to main content

dynamic_cli/validator/
range_validator.rs

1//! Numeric range validation functions
2//!
3//! This module provides functions to validate numeric values according to
4//! [`ValidationRule::Range`] constraints.
5//!
6//! # Functions
7//!
8//! - [`validate_range`] - Check if a value is within min/max bounds
9//!
10//! # Example
11//!
12//! ```
13//! use dynamic_cli::validator::range_validator::validate_range;
14//!
15//! // Validate value is between 0 and 100
16//! validate_range(50.0, "percentage", Some(0.0), Some(100.0))?;
17//!
18//! // Validate value is at least 0
19//! validate_range(5.0, "count", Some(0.0), None)?;
20//!
21//! // Validate value is at most 1.0
22//! validate_range(0.5, "probability", None, Some(1.0))?;
23//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
24//! ```
25
26use crate::error::{Result, ValidationError};
27
28/// Validate that a numeric value is within specified bounds
29///
30/// This function checks if a value satisfies the range constraints:
31/// - If `min` is specified: value >= min
32/// - If `max` is specified: value <= max
33/// - If both are specified: min <= value <= max
34///
35/// # Arguments
36///
37/// * `value` - The numeric value to validate
38/// * `arg_name` - Name of the argument (for error messages)
39/// * `min` - Optional minimum value (inclusive)
40/// * `max` - Optional maximum value (inclusive)
41///
42/// # Returns
43///
44/// - `Ok(())` if the value is within the specified range
45/// - `Err(ValidationError::OutOfRange)` if the value is outside the range
46///
47/// # Range Types
48///
49/// The function supports several range configurations:
50///
51/// - **Both bounds**: `validate_range(x, "arg", Some(0.0), Some(100.0))` → 0 ≤ x ≤ 100
52/// - **Lower bound only**: `validate_range(x, "arg", Some(0.0), None)` → x ≥ 0
53/// - **Upper bound only**: `validate_range(x, "arg", None, Some(100.0))` → x ≤ 100
54/// - **No bounds**: `validate_range(x, "arg", None, None)` → always valid
55///
56/// # Example
57///
58/// ```
59/// use dynamic_cli::validator::range_validator::validate_range;
60///
61/// // Validate percentage (0-100)
62/// assert!(validate_range(50.0, "percentage", Some(0.0), Some(100.0)).is_ok());
63/// assert!(validate_range(-10.0, "percentage", Some(0.0), Some(100.0)).is_err());
64/// assert!(validate_range(150.0, "percentage", Some(0.0), Some(100.0)).is_err());
65///
66/// // Validate non-negative count
67/// assert!(validate_range(5.0, "count", Some(0.0), None).is_ok());
68/// assert!(validate_range(-1.0, "count", Some(0.0), None).is_err());
69///
70/// // Validate probability (0-1)
71/// assert!(validate_range(0.5, "prob", Some(0.0), Some(1.0)).is_ok());
72/// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
73/// ```
74///
75/// # Error Messages
76///
77/// If the value is out of range, the error includes:
78/// - The argument name
79/// - The actual value
80/// - The expected range (min and max)
81///
82/// Example: `percentage must be between 0 and 100, got -10`
83///
84/// # Special Values
85///
86/// The function handles special floating-point values:
87/// - **Infinity**: Can be compared normally
88/// - **NaN**: Always fails validation (NaN comparisons always return false)
89///
90/// # Edge Cases
91///
92/// - If both `min` and `max` are `None`, validation always succeeds
93/// - Boundary values are **inclusive**: `validate_range(0.0, "x", Some(0.0), Some(1.0))` is valid
94/// - If `min > max`, this is a configuration error (should be caught by config validation)
95pub fn validate_range(
96    value: f64,
97    arg_name: &str,
98    min: Option<f64>,
99    max: Option<f64>,
100) -> Result<()> {
101    // Handle special case: NaN is never valid
102    // (NaN comparisons always return false, so it would fail anyway,
103    // but we make it explicit for clarity)
104    if value.is_nan() {
105        return Err(ValidationError::OutOfRange {
106            arg_name: arg_name.to_string(),
107            value,
108            min: min.unwrap_or(f64::NEG_INFINITY),
109            max: max.unwrap_or(f64::INFINITY),
110            suggestion: Some(
111                "Provide at least one of 'min' or 'max' in the validation rule.".to_string(),
112            ),
113        }
114        .into());
115    }
116
117    // Check minimum bound (inclusive)
118    if let Some(min_val) = min {
119        if value < min_val {
120            return Err(ValidationError::OutOfRange {
121                arg_name: arg_name.to_string(),
122                value,
123                min: min_val,
124                max: max.unwrap_or(f64::INFINITY),
125                suggestion: Some(format!(
126                    "Value must be greater than or equal to {}.",
127                    min_val
128                )),
129            }
130            .into());
131        }
132    }
133
134    // Check maximum bound (inclusive)
135    if let Some(max_val) = max {
136        if value > max_val {
137            return Err(ValidationError::OutOfRange {
138                arg_name: arg_name.to_string(),
139                value,
140                min: min.unwrap_or(f64::NEG_INFINITY),
141                max: max_val,
142                suggestion: Some(format!("Value must be less than or equal to {}.", max_val)),
143            }
144            .into());
145        }
146    }
147
148    Ok(())
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    // ========================================================================
156    // Tests for both bounds (min and max)
157    // ========================================================================
158
159    #[test]
160    fn test_validate_range_within_bounds() {
161        // Value in the middle of range
162        assert!(validate_range(50.0, "percentage", Some(0.0), Some(100.0)).is_ok());
163
164        // Value at lower boundary (inclusive)
165        assert!(validate_range(0.0, "percentage", Some(0.0), Some(100.0)).is_ok());
166
167        // Value at upper boundary (inclusive)
168        assert!(validate_range(100.0, "percentage", Some(0.0), Some(100.0)).is_ok());
169    }
170
171    #[test]
172    fn test_validate_range_below_minimum() {
173        let result = validate_range(-10.0, "percentage", Some(0.0), Some(100.0));
174
175        assert!(result.is_err());
176        match result.unwrap_err() {
177            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
178                arg_name,
179                value,
180                min,
181                max,
182                ..
183            }) => {
184                assert_eq!(arg_name, "percentage");
185                assert_eq!(value, -10.0);
186                assert_eq!(min, 0.0);
187                assert_eq!(max, 100.0);
188            }
189            other => panic!("Expected OutOfRange error, got {:?}", other),
190        }
191    }
192
193    #[test]
194    fn test_validate_range_above_maximum() {
195        let result = validate_range(150.0, "percentage", Some(0.0), Some(100.0));
196
197        assert!(result.is_err());
198        match result.unwrap_err() {
199            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
200                arg_name,
201                value,
202                min,
203                max,
204                ..
205            }) => {
206                assert_eq!(arg_name, "percentage");
207                assert_eq!(value, 150.0);
208                assert_eq!(min, 0.0);
209                assert_eq!(max, 100.0);
210            }
211            other => panic!("Expected OutOfRange error, got {:?}", other),
212        }
213    }
214
215    // ========================================================================
216    // Tests for minimum bound only
217    // ========================================================================
218
219    #[test]
220    fn test_validate_range_min_only_valid() {
221        // Value above minimum
222        assert!(validate_range(5.0, "count", Some(0.0), None).is_ok());
223
224        // Value at minimum (boundary)
225        assert!(validate_range(0.0, "count", Some(0.0), None).is_ok());
226
227        // Large positive value (no upper bound)
228        assert!(validate_range(1_000_000.0, "count", Some(0.0), None).is_ok());
229    }
230
231    #[test]
232    fn test_validate_range_min_only_invalid() {
233        let result = validate_range(-5.0, "count", Some(0.0), None);
234
235        assert!(result.is_err());
236        match result.unwrap_err() {
237            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
238                arg_name,
239                value,
240                min,
241                max,
242                ..
243            }) => {
244                assert_eq!(arg_name, "count");
245                assert_eq!(value, -5.0);
246                assert_eq!(min, 0.0);
247                assert!(max.is_infinite() && max.is_sign_positive()); // f64::INFINITY
248            }
249            other => panic!("Expected OutOfRange error, got {:?}", other),
250        }
251    }
252
253    // ========================================================================
254    // Tests for maximum bound only
255    // ========================================================================
256
257    #[test]
258    fn test_validate_range_max_only_valid() {
259        // Value below maximum
260        assert!(validate_range(0.5, "probability", None, Some(1.0)).is_ok());
261
262        // Value at maximum (boundary)
263        assert!(validate_range(1.0, "probability", None, Some(1.0)).is_ok());
264
265        // Large negative value (no lower bound)
266        assert!(validate_range(-1_000_000.0, "temperature", None, Some(100.0)).is_ok());
267    }
268
269    #[test]
270    fn test_validate_range_max_only_invalid() {
271        let result = validate_range(1.5, "probability", None, Some(1.0));
272
273        assert!(result.is_err());
274        match result.unwrap_err() {
275            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
276                arg_name,
277                value,
278                min,
279                max,
280                ..
281            }) => {
282                assert_eq!(arg_name, "probability");
283                assert_eq!(value, 1.5);
284                assert!(min.is_infinite() && min.is_sign_negative()); // f64::NEG_INFINITY
285                assert_eq!(max, 1.0);
286            }
287            other => panic!("Expected OutOfRange error, got {:?}", other),
288        }
289    }
290
291    // ========================================================================
292    // Tests for no bounds
293    // ========================================================================
294
295    #[test]
296    fn test_validate_range_no_bounds() {
297        // Any value should be valid with no bounds
298        assert!(validate_range(0.0, "value", None, None).is_ok());
299        assert!(validate_range(-1000.0, "value", None, None).is_ok());
300        assert!(validate_range(1000.0, "value", None, None).is_ok());
301        assert!(validate_range(f64::INFINITY, "value", None, None).is_ok());
302        assert!(validate_range(f64::NEG_INFINITY, "value", None, None).is_ok());
303    }
304
305    // ========================================================================
306    // Tests for special floating-point values
307    // ========================================================================
308
309    #[test]
310    fn test_validate_range_nan() {
311        // NaN should always fail validation
312        let result = validate_range(f64::NAN, "value", Some(0.0), Some(100.0));
313        assert!(result.is_err());
314
315        // Even with no bounds
316        let result = validate_range(f64::NAN, "value", None, None);
317        assert!(result.is_err());
318    }
319
320    #[test]
321    fn test_validate_range_infinity_positive() {
322        // Positive infinity should fail if max is finite
323        assert!(validate_range(f64::INFINITY, "value", Some(0.0), Some(100.0)).is_err());
324
325        // But succeed if no max
326        assert!(validate_range(f64::INFINITY, "value", Some(0.0), None).is_ok());
327
328        // And with no bounds
329        assert!(validate_range(f64::INFINITY, "value", None, None).is_ok());
330    }
331
332    #[test]
333    fn test_validate_range_infinity_negative() {
334        // Negative infinity should fail if min is finite
335        assert!(validate_range(f64::NEG_INFINITY, "value", Some(0.0), Some(100.0)).is_err());
336
337        // But succeed if no min
338        assert!(validate_range(f64::NEG_INFINITY, "value", None, Some(100.0)).is_ok());
339
340        // And with no bounds
341        assert!(validate_range(f64::NEG_INFINITY, "value", None, None).is_ok());
342    }
343
344    // ========================================================================
345    // Tests for boundary conditions
346    // ========================================================================
347
348    #[test]
349    fn test_validate_range_exact_boundaries() {
350        // Test that boundaries are inclusive
351
352        // Lower boundary
353        assert!(validate_range(0.0, "x", Some(0.0), Some(1.0)).is_ok());
354
355        // Upper boundary
356        assert!(validate_range(1.0, "x", Some(0.0), Some(1.0)).is_ok());
357
358        // Both boundaries at once
359        assert!(validate_range(0.0, "x", Some(0.0), Some(0.0)).is_ok()); // min == max == value
360    }
361
362    #[test]
363    fn test_validate_range_tiny_differences() {
364        // Test with very small differences (floating-point precision)
365
366        // Just inside range
367        assert!(validate_range(0.0000001, "x", Some(0.0), Some(1.0)).is_ok());
368        assert!(validate_range(0.9999999, "x", Some(0.0), Some(1.0)).is_ok());
369
370        // Just outside range
371        assert!(validate_range(-0.0000001, "x", Some(0.0), Some(1.0)).is_err());
372        assert!(validate_range(1.0000001, "x", Some(0.0), Some(1.0)).is_err());
373    }
374
375    // ========================================================================
376    // Tests for negative ranges
377    // ========================================================================
378
379    #[test]
380    fn test_validate_range_negative_values() {
381        // Range entirely in negative numbers
382        assert!(validate_range(-50.0, "temperature", Some(-100.0), Some(0.0)).is_ok());
383        assert!(validate_range(-100.0, "temperature", Some(-100.0), Some(0.0)).is_ok());
384        assert!(validate_range(0.0, "temperature", Some(-100.0), Some(0.0)).is_ok());
385
386        // Out of negative range
387        assert!(validate_range(-150.0, "temperature", Some(-100.0), Some(0.0)).is_err());
388        assert!(validate_range(10.0, "temperature", Some(-100.0), Some(0.0)).is_err());
389    }
390
391    // ========================================================================
392    // Tests for very large and very small values
393    // ========================================================================
394
395    #[test]
396    fn test_validate_range_large_values() {
397        let very_large = 1e308; // Close to f64::MAX
398
399        // Within large range
400        assert!(validate_range(very_large, "value", Some(0.0), None).is_ok());
401
402        // Outside large range
403        assert!(validate_range(very_large, "value", Some(0.0), Some(1e307)).is_err());
404    }
405
406    #[test]
407    fn test_validate_range_small_values() {
408        let very_small = 1e-308; // Close to f64::MIN_POSITIVE
409
410        // Within small range
411        assert!(validate_range(very_small, "value", Some(0.0), Some(1.0)).is_ok());
412
413        // Outside small range
414        assert!(validate_range(very_small, "value", Some(1e-307), Some(1.0)).is_err());
415    }
416
417    // ========================================================================
418    // Tests for zero
419    // ========================================================================
420
421    #[test]
422    fn test_validate_range_zero() {
423        // Zero at boundaries
424        assert!(validate_range(0.0, "x", Some(0.0), Some(1.0)).is_ok());
425        assert!(validate_range(0.0, "x", Some(-1.0), Some(0.0)).is_ok());
426
427        // Zero in middle
428        assert!(validate_range(0.0, "x", Some(-1.0), Some(1.0)).is_ok());
429
430        // Zero outside range
431        assert!(validate_range(0.0, "x", Some(1.0), Some(2.0)).is_err());
432        assert!(validate_range(0.0, "x", Some(-2.0), Some(-1.0)).is_err());
433    }
434
435    // ========================================================================
436    // Integration tests
437    // ========================================================================
438
439    #[test]
440    fn test_validate_range_realistic_scenarios() {
441        // Percentage (0-100)
442        assert!(validate_range(50.0, "percentage", Some(0.0), Some(100.0)).is_ok());
443        assert!(validate_range(-1.0, "percentage", Some(0.0), Some(100.0)).is_err());
444        assert!(validate_range(101.0, "percentage", Some(0.0), Some(100.0)).is_err());
445
446        // Probability (0-1)
447        assert!(validate_range(0.5, "probability", Some(0.0), Some(1.0)).is_ok());
448        assert!(validate_range(1.5, "probability", Some(0.0), Some(1.0)).is_err());
449
450        // Temperature in Celsius (-273.15 to infinity)
451        assert!(validate_range(-100.0, "temperature", Some(-273.15), None).is_ok());
452        assert!(validate_range(-300.0, "temperature", Some(-273.15), None).is_err());
453
454        // Age (0 to 150)
455        assert!(validate_range(25.0, "age", Some(0.0), Some(150.0)).is_ok());
456        assert!(validate_range(200.0, "age", Some(0.0), Some(150.0)).is_err());
457    }
458}