Skip to main content

tanzim_validate/
integer.rs

1use crate::error::{Error, ErrorKind};
2use crate::number::{Sign, check_sign};
3use crate::{Meta, Validator};
4use tanzim_value::{Value, ValueType};
5
6/// Convert an integral `f64` into an `isize`, or `None` if it has a fraction or
7/// falls outside the representable range.
8fn f64_to_isize(number: f64) -> Option<isize> {
9    if number.fract() != 0.0 {
10        return None;
11    }
12    if number < isize::MIN as f64 || number > isize::MAX as f64 {
13        return None;
14    }
15    Some(number as isize)
16}
17
18/// (`integer` feature) Accepts an integer, with optional inclusive bounds and lenient coercion.
19///
20/// Coercion:
21/// - an integer stays as-is;
22/// - a string is parsed as an integer, or as an integral float (e.g. `"3.0"`);
23/// - a float with no fractional part (e.g. `3.0`) becomes an integer.
24#[derive(Debug, Clone, Default)]
25pub struct Integer {
26    meta: Meta,
27    min: Option<isize>,
28    max: Option<isize>,
29    sign: Option<Sign>,
30}
31
32impl Integer {
33    /// Attach human-facing metadata (name, description, examples, default, output conversion).
34    pub fn with_meta(mut self, meta: Meta) -> Self {
35        self.meta = meta;
36        self
37    }
38
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    pub fn min(mut self, min: isize) -> Self {
44        self.min = Some(min);
45        self
46    }
47
48    pub fn max(mut self, max: isize) -> Self {
49        self.max = Some(max);
50        self
51    }
52
53    pub fn range(mut self, start: isize, end: isize) -> Self {
54        self.min = Some(start);
55        self.max = Some(end);
56        self
57    }
58
59    /// Require the value to be strictly greater than zero.
60    pub fn positive(mut self) -> Self {
61        self.sign = Some(Sign::Positive);
62        self
63    }
64
65    /// Require the value to be greater than or equal to zero.
66    pub fn non_negative(mut self) -> Self {
67        self.sign = Some(Sign::NonNegative);
68        self
69    }
70
71    /// Require the value to be strictly less than zero.
72    pub fn negative(mut self) -> Self {
73        self.sign = Some(Sign::Negative);
74        self
75    }
76
77    /// Require the value to be less than or equal to zero.
78    pub fn non_positive(mut self) -> Self {
79        self.sign = Some(Sign::NonPositive);
80        self
81    }
82}
83
84crate::impl_meta_methods!(Integer);
85
86impl Validator for Integer {
87    fn meta(&self) -> &Meta {
88        &self.meta
89    }
90
91    fn meta_mut(&mut self) -> &mut Meta {
92        &mut self.meta
93    }
94
95    fn check(&self, value: &mut Value) -> Result<(), Error> {
96        let coerced = match value {
97            Value::Int(number) => *number,
98            Value::Float(number) => match f64_to_isize(*number) {
99                Some(number) => number,
100                None => {
101                    return Err(Error::new(ErrorKind::NotConvertible {
102                        target: ValueType::Int,
103                        found: ValueType::Float,
104                    }));
105                }
106            },
107            Value::String(text) => {
108                if let Ok(number) = text.parse::<isize>() {
109                    number
110                } else if let Ok(number) = text.parse::<f64>() {
111                    match f64_to_isize(number) {
112                        Some(number) => number,
113                        None => {
114                            return Err(Error::new(ErrorKind::NotConvertible {
115                                target: ValueType::Int,
116                                found: ValueType::String,
117                            }));
118                        }
119                    }
120                } else {
121                    return Err(Error::new(ErrorKind::NotConvertible {
122                        target: ValueType::Int,
123                        found: ValueType::String,
124                    }));
125                }
126            }
127            other => {
128                return Err(Error::new(ErrorKind::Type {
129                    expected: ValueType::Int,
130                    found: other.type_name(),
131                }));
132            }
133        };
134
135        if let Some(min) = self.min
136            && coerced < min
137        {
138            return Err(Error::new(ErrorKind::BelowMin {
139                value: coerced.to_string(),
140                min: min.to_string(),
141            }));
142        }
143        if let Some(max) = self.max
144            && coerced > max
145        {
146            return Err(Error::new(ErrorKind::AboveMax {
147                value: coerced.to_string(),
148                max: max.to_string(),
149            }));
150        }
151
152        check_sign(self.sign, coerced as f64)?;
153
154        *value = Value::Int(coerced);
155        Ok(())
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn accepts_integer_in_range() {
165        let mut value = Value::Int(50);
166        assert!(Integer::new().range(0, 100).validate(&mut value).is_ok());
167    }
168
169    #[test]
170    fn rejects_out_of_range() {
171        let mut value = Value::Int(200);
172        let error = Integer::new().max(100).validate(&mut value).unwrap_err();
173        assert!(matches!(error.kind, ErrorKind::AboveMax { .. }));
174    }
175
176    #[test]
177    fn coerces_integer_string() {
178        let mut value = Value::String("42".into());
179        Integer::new().validate(&mut value).unwrap();
180        assert_eq!(value, Value::Int(42));
181    }
182
183    #[test]
184    fn coerces_integral_float_string() {
185        let mut value = Value::String("3.0".into());
186        Integer::new().validate(&mut value).unwrap();
187        assert_eq!(value, Value::Int(3));
188    }
189
190    #[test]
191    fn coerces_integral_float() {
192        let mut value = Value::Float(7.0);
193        Integer::new().validate(&mut value).unwrap();
194        assert_eq!(value, Value::Int(7));
195    }
196
197    #[test]
198    fn rejects_fractional_float() {
199        let mut value = Value::Float(3.5);
200        let error = Integer::new().validate(&mut value).unwrap_err();
201        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
202    }
203
204    #[test]
205    fn rejects_non_numeric_string() {
206        let mut value = Value::String("abc".into());
207        let error = Integer::new().validate(&mut value).unwrap_err();
208        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
209    }
210}