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        }
111        .into());
112    }
113
114    // Check minimum bound (inclusive)
115    if let Some(min_val) = min {
116        if value < min_val {
117            return Err(ValidationError::OutOfRange {
118                arg_name: arg_name.to_string(),
119                value,
120                min: min_val,
121                max: max.unwrap_or(f64::INFINITY),
122            }
123            .into());
124        }
125    }
126
127    // Check maximum bound (inclusive)
128    if let Some(max_val) = max {
129        if value > max_val {
130            return Err(ValidationError::OutOfRange {
131                arg_name: arg_name.to_string(),
132                value,
133                min: min.unwrap_or(f64::NEG_INFINITY),
134                max: max_val,
135            }
136            .into());
137        }
138    }
139
140    Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    // ========================================================================
148    // Tests for both bounds (min and max)
149    // ========================================================================
150
151    #[test]
152    fn test_validate_range_within_bounds() {
153        // Value in the middle of range
154        assert!(validate_range(50.0, "percentage", Some(0.0), Some(100.0)).is_ok());
155
156        // Value at lower boundary (inclusive)
157        assert!(validate_range(0.0, "percentage", Some(0.0), Some(100.0)).is_ok());
158
159        // Value at upper boundary (inclusive)
160        assert!(validate_range(100.0, "percentage", Some(0.0), Some(100.0)).is_ok());
161    }
162
163    #[test]
164    fn test_validate_range_below_minimum() {
165        let result = validate_range(-10.0, "percentage", Some(0.0), Some(100.0));
166
167        assert!(result.is_err());
168        match result.unwrap_err() {
169            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
170                arg_name,
171                value,
172                min,
173                max,
174            }) => {
175                assert_eq!(arg_name, "percentage");
176                assert_eq!(value, -10.0);
177                assert_eq!(min, 0.0);
178                assert_eq!(max, 100.0);
179            }
180            other => panic!("Expected OutOfRange error, got {:?}", other),
181        }
182    }
183
184    #[test]
185    fn test_validate_range_above_maximum() {
186        let result = validate_range(150.0, "percentage", Some(0.0), Some(100.0));
187
188        assert!(result.is_err());
189        match result.unwrap_err() {
190            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
191                arg_name,
192                value,
193                min,
194                max,
195            }) => {
196                assert_eq!(arg_name, "percentage");
197                assert_eq!(value, 150.0);
198                assert_eq!(min, 0.0);
199                assert_eq!(max, 100.0);
200            }
201            other => panic!("Expected OutOfRange error, got {:?}", other),
202        }
203    }
204
205    // ========================================================================
206    // Tests for minimum bound only
207    // ========================================================================
208
209    #[test]
210    fn test_validate_range_min_only_valid() {
211        // Value above minimum
212        assert!(validate_range(5.0, "count", Some(0.0), None).is_ok());
213
214        // Value at minimum (boundary)
215        assert!(validate_range(0.0, "count", Some(0.0), None).is_ok());
216
217        // Large positive value (no upper bound)
218        assert!(validate_range(1_000_000.0, "count", Some(0.0), None).is_ok());
219    }
220
221    #[test]
222    fn test_validate_range_min_only_invalid() {
223        let result = validate_range(-5.0, "count", Some(0.0), None);
224
225        assert!(result.is_err());
226        match result.unwrap_err() {
227            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
228                arg_name,
229                value,
230                min,
231                max,
232            }) => {
233                assert_eq!(arg_name, "count");
234                assert_eq!(value, -5.0);
235                assert_eq!(min, 0.0);
236                assert!(max.is_infinite() && max.is_sign_positive()); // f64::INFINITY
237            }
238            other => panic!("Expected OutOfRange error, got {:?}", other),
239        }
240    }
241
242    // ========================================================================
243    // Tests for maximum bound only
244    // ========================================================================
245
246    #[test]
247    fn test_validate_range_max_only_valid() {
248        // Value below maximum
249        assert!(validate_range(0.5, "probability", None, Some(1.0)).is_ok());
250
251        // Value at maximum (boundary)
252        assert!(validate_range(1.0, "probability", None, Some(1.0)).is_ok());
253
254        // Large negative value (no lower bound)
255        assert!(validate_range(-1_000_000.0, "temperature", None, Some(100.0)).is_ok());
256    }
257
258    #[test]
259    fn test_validate_range_max_only_invalid() {
260        let result = validate_range(1.5, "probability", None, Some(1.0));
261
262        assert!(result.is_err());
263        match result.unwrap_err() {
264            crate::error::DynamicCliError::Validation(ValidationError::OutOfRange {
265                arg_name,
266                value,
267                min,
268                max,
269            }) => {
270                assert_eq!(arg_name, "probability");
271                assert_eq!(value, 1.5);
272                assert!(min.is_infinite() && min.is_sign_negative()); // f64::NEG_INFINITY
273                assert_eq!(max, 1.0);
274            }
275            other => panic!("Expected OutOfRange error, got {:?}", other),
276        }
277    }
278
279    // ========================================================================
280    // Tests for no bounds
281    // ========================================================================
282
283    #[test]
284    fn test_validate_range_no_bounds() {
285        // Any value should be valid with no bounds
286        assert!(validate_range(0.0, "value", None, None).is_ok());
287        assert!(validate_range(-1000.0, "value", None, None).is_ok());
288        assert!(validate_range(1000.0, "value", None, None).is_ok());
289        assert!(validate_range(f64::INFINITY, "value", None, None).is_ok());
290        assert!(validate_range(f64::NEG_INFINITY, "value", None, None).is_ok());
291    }
292
293    // ========================================================================
294    // Tests for special floating-point values
295    // ========================================================================
296
297    #[test]
298    fn test_validate_range_nan() {
299        // NaN should always fail validation
300        let result = validate_range(f64::NAN, "value", Some(0.0), Some(100.0));
301        assert!(result.is_err());
302
303        // Even with no bounds
304        let result = validate_range(f64::NAN, "value", None, None);
305        assert!(result.is_err());
306    }
307
308    #[test]
309    fn test_validate_range_infinity_positive() {
310        // Positive infinity should fail if max is finite
311        assert!(validate_range(f64::INFINITY, "value", Some(0.0), Some(100.0)).is_err());
312
313        // But succeed if no max
314        assert!(validate_range(f64::INFINITY, "value", Some(0.0), None).is_ok());
315
316        // And with no bounds
317        assert!(validate_range(f64::INFINITY, "value", None, None).is_ok());
318    }
319
320    #[test]
321    fn test_validate_range_infinity_negative() {
322        // Negative infinity should fail if min is finite
323        assert!(validate_range(f64::NEG_INFINITY, "value", Some(0.0), Some(100.0)).is_err());
324
325        // But succeed if no min
326        assert!(validate_range(f64::NEG_INFINITY, "value", None, Some(100.0)).is_ok());
327
328        // And with no bounds
329        assert!(validate_range(f64::NEG_INFINITY, "value", None, None).is_ok());
330    }
331
332    // ========================================================================
333    // Tests for boundary conditions
334    // ========================================================================
335
336    #[test]
337    fn test_validate_range_exact_boundaries() {
338        // Test that boundaries are inclusive
339
340        // Lower boundary
341        assert!(validate_range(0.0, "x", Some(0.0), Some(1.0)).is_ok());
342
343        // Upper boundary
344        assert!(validate_range(1.0, "x", Some(0.0), Some(1.0)).is_ok());
345
346        // Both boundaries at once
347        assert!(validate_range(0.0, "x", Some(0.0), Some(0.0)).is_ok()); // min == max == value
348    }
349
350    #[test]
351    fn test_validate_range_tiny_differences() {
352        // Test with very small differences (floating-point precision)
353
354        // Just inside range
355        assert!(validate_range(0.0000001, "x", Some(0.0), Some(1.0)).is_ok());
356        assert!(validate_range(0.9999999, "x", Some(0.0), Some(1.0)).is_ok());
357
358        // Just outside range
359        assert!(validate_range(-0.0000001, "x", Some(0.0), Some(1.0)).is_err());
360        assert!(validate_range(1.0000001, "x", Some(0.0), Some(1.0)).is_err());
361    }
362
363    // ========================================================================
364    // Tests for negative ranges
365    // ========================================================================
366
367    #[test]
368    fn test_validate_range_negative_values() {
369        // Range entirely in negative numbers
370        assert!(validate_range(-50.0, "temperature", Some(-100.0), Some(0.0)).is_ok());
371        assert!(validate_range(-100.0, "temperature", Some(-100.0), Some(0.0)).is_ok());
372        assert!(validate_range(0.0, "temperature", Some(-100.0), Some(0.0)).is_ok());
373
374        // Out of negative range
375        assert!(validate_range(-150.0, "temperature", Some(-100.0), Some(0.0)).is_err());
376        assert!(validate_range(10.0, "temperature", Some(-100.0), Some(0.0)).is_err());
377    }
378
379    // ========================================================================
380    // Tests for very large and very small values
381    // ========================================================================
382
383    #[test]
384    fn test_validate_range_large_values() {
385        let very_large = 1e308; // Close to f64::MAX
386
387        // Within large range
388        assert!(validate_range(very_large, "value", Some(0.0), None).is_ok());
389
390        // Outside large range
391        assert!(validate_range(very_large, "value", Some(0.0), Some(1e307)).is_err());
392    }
393
394    #[test]
395    fn test_validate_range_small_values() {
396        let very_small = 1e-308; // Close to f64::MIN_POSITIVE
397
398        // Within small range
399        assert!(validate_range(very_small, "value", Some(0.0), Some(1.0)).is_ok());
400
401        // Outside small range
402        assert!(validate_range(very_small, "value", Some(1e-307), Some(1.0)).is_err());
403    }
404
405    // ========================================================================
406    // Tests for zero
407    // ========================================================================
408
409    #[test]
410    fn test_validate_range_zero() {
411        // Zero at boundaries
412        assert!(validate_range(0.0, "x", Some(0.0), Some(1.0)).is_ok());
413        assert!(validate_range(0.0, "x", Some(-1.0), Some(0.0)).is_ok());
414
415        // Zero in middle
416        assert!(validate_range(0.0, "x", Some(-1.0), Some(1.0)).is_ok());
417
418        // Zero outside range
419        assert!(validate_range(0.0, "x", Some(1.0), Some(2.0)).is_err());
420        assert!(validate_range(0.0, "x", Some(-2.0), Some(-1.0)).is_err());
421    }
422
423    // ========================================================================
424    // Integration tests
425    // ========================================================================
426
427    #[test]
428    fn test_validate_range_realistic_scenarios() {
429        // Percentage (0-100)
430        assert!(validate_range(50.0, "percentage", Some(0.0), Some(100.0)).is_ok());
431        assert!(validate_range(-1.0, "percentage", Some(0.0), Some(100.0)).is_err());
432        assert!(validate_range(101.0, "percentage", Some(0.0), Some(100.0)).is_err());
433
434        // Probability (0-1)
435        assert!(validate_range(0.5, "probability", Some(0.0), Some(1.0)).is_ok());
436        assert!(validate_range(1.5, "probability", Some(0.0), Some(1.0)).is_err());
437
438        // Temperature in Celsius (-273.15 to infinity)
439        assert!(validate_range(-100.0, "temperature", Some(-273.15), None).is_ok());
440        assert!(validate_range(-300.0, "temperature", Some(-273.15), None).is_err());
441
442        // Age (0 to 150)
443        assert!(validate_range(25.0, "age", Some(0.0), Some(150.0)).is_ok());
444        assert!(validate_range(200.0, "age", Some(0.0), Some(150.0)).is_err());
445    }
446}