Skip to main content

karbon_framework/validation/constraints/number/
range.rs

1use crate::validation::constraints::{ConstraintResult, ConstraintViolation, NumericConstraint};
2
3/// Validates that a number is within a given range.
4///
5/// Equivalent to Symfony's `Range` constraint.
6pub struct Range {
7    pub min: Option<f64>,
8    pub max: Option<f64>,
9    pub min_message: String,
10    pub max_message: String,
11}
12
13impl Default for Range {
14    fn default() -> Self {
15        Self {
16            min: None,
17            max: None,
18            min_message: "This value should be {{ limit }} or more.".to_string(),
19            max_message: "This value should be {{ limit }} or less.".to_string(),
20        }
21    }
22}
23
24impl Range {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn min(mut self, min: f64) -> Self {
30        self.min = Some(min);
31        self
32    }
33
34    pub fn max(mut self, max: f64) -> Self {
35        self.max = Some(max);
36        self
37    }
38
39    pub fn between(min: f64, max: f64) -> Self {
40        Self {
41            min: Some(min),
42            max: Some(max),
43            ..Self::default()
44        }
45    }
46
47    pub fn with_min_message(mut self, message: impl Into<String>) -> Self {
48        self.min_message = message.into();
49        self
50    }
51
52    pub fn with_max_message(mut self, message: impl Into<String>) -> Self {
53        self.max_message = message.into();
54        self
55    }
56
57    fn format_message(template: &str, limit: f64) -> String {
58        template.replace("{{ limit }}", &limit.to_string())
59    }
60}
61
62impl NumericConstraint for Range {
63    fn validate_f64(&self, value: f64) -> ConstraintResult {
64        if let Some(min) = self.min {
65            if value < min {
66                return Err(ConstraintViolation::new(
67                    self.name(),
68                    Self::format_message(&self.min_message, min),
69                    value.to_string(),
70                ));
71            }
72        }
73
74        if let Some(max) = self.max {
75            if value > max {
76                return Err(ConstraintViolation::new(
77                    self.name(),
78                    Self::format_message(&self.max_message, max),
79                    value.to_string(),
80                ));
81            }
82        }
83
84        Ok(())
85    }
86
87    fn name(&self) -> &'static str {
88        "Range"
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_within_range() {
98        let constraint = Range::between(1.0, 100.0);
99        assert!(constraint.validate_f64(1.0).is_ok());
100        assert!(constraint.validate_f64(50.0).is_ok());
101        assert!(constraint.validate_f64(100.0).is_ok());
102    }
103
104    #[test]
105    fn test_out_of_range() {
106        let constraint = Range::between(1.0, 100.0);
107        assert!(constraint.validate_f64(0.0).is_err());
108        assert!(constraint.validate_f64(101.0).is_err());
109        assert!(constraint.validate_f64(-5.0).is_err());
110    }
111
112    #[test]
113    fn test_min_only() {
114        let constraint = Range::new().min(0.0);
115        assert!(constraint.validate_f64(0.0).is_ok());
116        assert!(constraint.validate_f64(1000.0).is_ok());
117        assert!(constraint.validate_f64(-1.0).is_err());
118    }
119
120    #[test]
121    fn test_max_only() {
122        let constraint = Range::new().max(100.0);
123        assert!(constraint.validate_f64(-1000.0).is_ok());
124        assert!(constraint.validate_f64(100.0).is_ok());
125        assert!(constraint.validate_f64(101.0).is_err());
126    }
127
128    #[test]
129    fn test_float_values() {
130        let constraint = Range::between(0.0, 1.0);
131        assert!(constraint.validate_f64(0.5).is_ok());
132        assert!(constraint.validate_f64(0.99).is_ok());
133        assert!(constraint.validate_f64(1.01).is_err());
134    }
135}