skp_validator_rules/numeric/
range.rs

1//! Numeric range validation rule.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5/// Numeric range validation rule.
6///
7/// Supports inclusive and exclusive bounds (JSON Schema compatible).
8///
9/// # Example
10///
11/// ```rust
12/// use skp_validator_rules::numeric::range::RangeRule;
13/// use skp_validator_core::{Rule, ValidationContext};
14///
15/// let rule = RangeRule::<f64>::new().min(0.0).max(100.0);
16/// let ctx = ValidationContext::default();
17///
18/// assert!(rule.validate(&50.0, &ctx).is_ok());
19/// assert!(rule.validate(&-1.0, &ctx).is_err());
20/// ```
21#[derive(Debug, Clone)]
22pub struct RangeRule<T> {
23    /// Minimum value (inclusive by default)
24    pub min: Option<T>,
25    /// Maximum value (inclusive by default)
26    pub max: Option<T>,
27    /// Whether min is exclusive
28    pub exclusive_min: bool,
29    /// Whether max is exclusive
30    pub exclusive_max: bool,
31    /// Custom error message
32    pub message: Option<String>,
33}
34
35impl<T> RangeRule<T> {
36    /// Create a new range rule.
37    pub fn new() -> Self {
38        Self {
39            min: None,
40            max: None,
41            exclusive_min: false,
42            exclusive_max: false,
43            message: None,
44        }
45    }
46
47    /// Set minimum value (inclusive).
48    pub fn min(mut self, min: T) -> Self {
49        self.min = Some(min);
50        self
51    }
52
53    /// Set maximum value (inclusive).
54    pub fn max(mut self, max: T) -> Self {
55        self.max = Some(max);
56        self
57    }
58
59    /// Set exclusive minimum.
60    pub fn exclusive_min(mut self, min: T) -> Self {
61        self.min = Some(min);
62        self.exclusive_min = true;
63        self
64    }
65
66    /// Set exclusive maximum.
67    pub fn exclusive_max(mut self, max: T) -> Self {
68        self.max = Some(max);
69        self.exclusive_max = true;
70        self
71    }
72
73    /// Set custom error message.
74    pub fn message(mut self, msg: impl Into<String>) -> Self {
75        self.message = Some(msg.into());
76        self
77    }
78}
79
80impl<T> Default for RangeRule<T> {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86macro_rules! impl_range_rule {
87    ($($t:ty),+) => {
88        $(
89            impl Rule<$t> for RangeRule<$t> {
90                fn validate(&self, value: &$t, _ctx: &ValidationContext) -> ValidationResult<()> {
91                    // Check minimum
92                    if let Some(ref min) = self.min {
93                        let failed = if self.exclusive_min {
94                            value <= min
95                        } else {
96                            value < min
97                        };
98                        
99                        if failed {
100                            let msg = self.message.clone().unwrap_or_else(|| {
101                                if self.exclusive_min {
102                                    format!("Must be greater than {}", min)
103                                } else {
104                                    format!("Must be at least {}", min)
105                                }
106                            });
107                            return Err(ValidationErrors::from_iter([
108                                ValidationError::root("range.min", msg)
109                            ]));
110                        }
111                    }
112                    
113                    // Check maximum
114                    if let Some(ref max) = self.max {
115                        let failed = if self.exclusive_max {
116                            value >= max
117                        } else {
118                            value > max
119                        };
120                        
121                        if failed {
122                            let msg = self.message.clone().unwrap_or_else(|| {
123                                if self.exclusive_max {
124                                    format!("Must be less than {}", max)
125                                } else {
126                                    format!("Must be at most {}", max)
127                                }
128                            });
129                            return Err(ValidationErrors::from_iter([
130                                ValidationError::root("range.max", msg)
131                            ]));
132                        }
133                    }
134                    
135                    Ok(())
136                }
137
138                fn name(&self) -> &'static str {
139                    "range"
140                }
141
142                fn default_message(&self) -> String {
143                    "Value out of range".to_string()
144                }
145            }
146        )+
147    };
148}
149
150impl_range_rule!(i8, i16, i32, i64, i128, isize);
151impl_range_rule!(u8, u16, u32, u64, u128, usize);
152impl_range_rule!(f32, f64);
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_range_f64() {
160        let rule = RangeRule::<f64>::new().min(0.0).max(100.0);
161        let ctx = ValidationContext::default();
162
163        assert!(rule.validate(&0.0, &ctx).is_ok());
164        assert!(rule.validate(&50.0, &ctx).is_ok());
165        assert!(rule.validate(&100.0, &ctx).is_ok());
166        assert!(rule.validate(&-1.0, &ctx).is_err());
167        assert!(rule.validate(&101.0, &ctx).is_err());
168    }
169
170    #[test]
171    fn test_range_i32() {
172        let rule = RangeRule::<i32>::new().min(18).max(120);
173        let ctx = ValidationContext::default();
174
175        assert!(rule.validate(&18, &ctx).is_ok());
176        assert!(rule.validate(&30, &ctx).is_ok());
177        assert!(rule.validate(&17, &ctx).is_err());
178        assert!(rule.validate(&121, &ctx).is_err());
179    }
180
181    #[test]
182    fn test_exclusive() {
183        let rule = RangeRule::<i32>::new().exclusive_min(0).exclusive_max(10);
184        let ctx = ValidationContext::default();
185
186        assert!(rule.validate(&0, &ctx).is_err());   // 0 is excluded
187        assert!(rule.validate(&1, &ctx).is_ok());
188        assert!(rule.validate(&9, &ctx).is_ok());
189        assert!(rule.validate(&10, &ctx).is_err());  // 10 is excluded
190    }
191}