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
84impl Validator for Integer {
85    fn meta(&self) -> &Meta {
86        &self.meta
87    }
88
89    fn meta_mut(&mut self) -> &mut Meta {
90        &mut self.meta
91    }
92
93    fn check(&self, value: &mut Value) -> Result<(), Error> {
94        let coerced = match value {
95            Value::Int(number) => *number,
96            Value::Float(number) => match f64_to_isize(*number) {
97                Some(number) => number,
98                None => {
99                    return Err(Error::new(ErrorKind::NotConvertible {
100                        target: ValueType::Int,
101                        found: ValueType::Float,
102                    }));
103                }
104            },
105            Value::String(text) => {
106                if let Ok(number) = text.parse::<isize>() {
107                    number
108                } else if let Ok(number) = text.parse::<f64>() {
109                    match f64_to_isize(number) {
110                        Some(number) => number,
111                        None => {
112                            return Err(Error::new(ErrorKind::NotConvertible {
113                                target: ValueType::Int,
114                                found: ValueType::String,
115                            }));
116                        }
117                    }
118                } else {
119                    return Err(Error::new(ErrorKind::NotConvertible {
120                        target: ValueType::Int,
121                        found: ValueType::String,
122                    }));
123                }
124            }
125            other => {
126                return Err(Error::new(ErrorKind::Type {
127                    expected: ValueType::Int,
128                    found: other.type_name(),
129                }));
130            }
131        };
132
133        if let Some(min) = self.min
134            && coerced < min
135        {
136            return Err(Error::new(ErrorKind::BelowMin {
137                value: coerced.to_string(),
138                min: min.to_string(),
139            }));
140        }
141        if let Some(max) = self.max
142            && coerced > max
143        {
144            return Err(Error::new(ErrorKind::AboveMax {
145                value: coerced.to_string(),
146                max: max.to_string(),
147            }));
148        }
149
150        check_sign(self.sign, coerced as f64)?;
151
152        *value = Value::Int(coerced);
153        Ok(())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn accepts_integer_in_range() {
163        let mut value = Value::Int(50);
164        assert!(Integer::new().range(0, 100).validate(&mut value).is_ok());
165    }
166
167    #[test]
168    fn rejects_out_of_range() {
169        let mut value = Value::Int(200);
170        let error = Integer::new().max(100).validate(&mut value).unwrap_err();
171        assert!(matches!(error.kind, ErrorKind::AboveMax { .. }));
172    }
173
174    #[test]
175    fn coerces_integer_string() {
176        let mut value = Value::String("42".into());
177        Integer::new().validate(&mut value).unwrap();
178        assert_eq!(value, Value::Int(42));
179    }
180
181    #[test]
182    fn coerces_integral_float_string() {
183        let mut value = Value::String("3.0".into());
184        Integer::new().validate(&mut value).unwrap();
185        assert_eq!(value, Value::Int(3));
186    }
187
188    #[test]
189    fn coerces_integral_float() {
190        let mut value = Value::Float(7.0);
191        Integer::new().validate(&mut value).unwrap();
192        assert_eq!(value, Value::Int(7));
193    }
194
195    #[test]
196    fn rejects_fractional_float() {
197        let mut value = Value::Float(3.5);
198        let error = Integer::new().validate(&mut value).unwrap_err();
199        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
200    }
201
202    #[test]
203    fn rejects_non_numeric_string() {
204        let mut value = Value::String("abc".into());
205        let error = Integer::new().validate(&mut value).unwrap_err();
206        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
207    }
208}