Skip to main content

rusdantic_core/rules/
range.rs

1//! Numeric range validation rule.
2//!
3//! Validates that a numeric value falls within the specified bounds.
4//! Works with any type that implements `PartialOrd` and `Display`.
5
6use crate::error::{PathSegment, ValidationError, ValidationErrors};
7use std::fmt::Display;
8
9/// Validate that the value is within the specified numeric range.
10///
11/// - `min`: Minimum value (inclusive). `None` means no lower bound.
12/// - `max`: Maximum value (inclusive). `None` means no upper bound.
13///
14/// Works with all Rust numeric types: i8, i16, i32, i64, i128, u8, u16, u32,
15/// u64, u128, f32, f64, isize, usize.
16/// Validate that the value is within the specified numeric range.
17///
18/// Works with all Rust numeric types including i128/u128.
19/// Uses `Display` for error message formatting instead of `Into<serde_json::Value>`
20/// to avoid trait bound issues with 128-bit integers.
21pub fn validate_range<T: PartialOrd + Display + Copy>(
22    value: &T,
23    min: Option<T>,
24    max: Option<T>,
25    path: &[PathSegment],
26    errors: &mut ValidationErrors,
27) {
28    // Check for NaN: PartialOrd where value != value indicates NaN.
29    // NaN silently passes range checks because all comparisons return false.
30    if value.partial_cmp(value).is_none() {
31        errors.add(
32            ValidationError::new("range_nan", "value is NaN (not a number)")
33                .with_path(path.to_vec()),
34        );
35        return;
36    }
37
38    if let Some(min_val) = min {
39        if *value < min_val {
40            errors.add(
41                ValidationError::new(
42                    "range_min",
43                    format!("must be at least {}", min_val),
44                )
45                .with_path(path.to_vec())
46                .with_param("min", serde_json::Value::String(min_val.to_string()))
47                .with_param("actual", serde_json::Value::String(value.to_string())),
48            );
49        }
50    }
51
52    if let Some(max_val) = max {
53        if *value > max_val {
54            errors.add(
55                ValidationError::new(
56                    "range_max",
57                    format!("must be at most {}", max_val),
58                )
59                .with_path(path.to_vec())
60                .with_param("max", serde_json::Value::String(max_val.to_string()))
61                .with_param("actual", serde_json::Value::String(value.to_string())),
62            );
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    fn path(name: &str) -> Vec<PathSegment> {
72        vec![PathSegment::Field(name.to_string())]
73    }
74
75    #[test]
76    fn test_u8_range_valid() {
77        let mut errors = ValidationErrors::new();
78        validate_range(&25u8, Some(18u8), Some(120u8), &path("age"), &mut errors);
79        assert!(errors.is_empty());
80    }
81
82    #[test]
83    fn test_u8_range_below_min() {
84        let mut errors = ValidationErrors::new();
85        validate_range(&16u8, Some(18u8), None, &path("age"), &mut errors);
86        assert_eq!(errors.len(), 1);
87        assert_eq!(errors.errors()[0].code, "range_min");
88    }
89
90    #[test]
91    fn test_u8_range_above_max() {
92        let mut errors = ValidationErrors::new();
93        validate_range(&200u8, None, Some(150u8), &path("age"), &mut errors);
94        assert_eq!(errors.len(), 1);
95        assert_eq!(errors.errors()[0].code, "range_max");
96    }
97
98    #[test]
99    fn test_i32_range() {
100        let mut errors = ValidationErrors::new();
101        validate_range(&-5i32, Some(-10i32), Some(10i32), &path("temp"), &mut errors);
102        assert!(errors.is_empty());
103    }
104
105    #[test]
106    fn test_i64_range_negative() {
107        let mut errors = ValidationErrors::new();
108        validate_range(&-20i64, Some(-10i64), None, &path("offset"), &mut errors);
109        assert_eq!(errors.len(), 1);
110    }
111
112    #[test]
113    fn test_f64_range() {
114        let mut errors = ValidationErrors::new();
115        validate_range(&3.15f64, Some(0.0f64), Some(10.0f64), &path("ratio"), &mut errors);
116        assert!(errors.is_empty());
117    }
118
119    #[test]
120    fn test_f64_range_below() {
121        let mut errors = ValidationErrors::new();
122        validate_range(&-0.1f64, Some(0.0f64), None, &path("ratio"), &mut errors);
123        assert_eq!(errors.len(), 1);
124    }
125
126    #[test]
127    fn test_boundary_values() {
128        let mut errors = ValidationErrors::new();
129        // Exactly at min should be valid
130        validate_range(&18u8, Some(18u8), Some(120u8), &path("age"), &mut errors);
131        assert!(errors.is_empty());
132
133        // Exactly at max should be valid
134        validate_range(&120u8, Some(18u8), Some(120u8), &path("age"), &mut errors);
135        assert!(errors.is_empty());
136    }
137
138    #[test]
139    fn test_no_bounds() {
140        let mut errors = ValidationErrors::new();
141        validate_range::<i32>(&999, None, None, &path("f"), &mut errors);
142        assert!(errors.is_empty());
143    }
144
145    #[test]
146    fn test_usize_range() {
147        let mut errors = ValidationErrors::new();
148        validate_range(&5usize, Some(1usize), Some(100usize), &path("count"), &mut errors);
149        assert!(errors.is_empty());
150    }
151}