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}