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}