Skip to main content

tanzim_validate/
number.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::Value;
4
5/// A sign constraint shared by [`Number`], [`crate::Integer`], and [`crate::Float`].
6#[derive(Debug, Clone, Copy)]
7pub(crate) enum Sign {
8    Positive,
9    NonNegative,
10    Negative,
11    NonPositive,
12}
13
14/// Check a numeric value against an optional sign constraint.
15pub(crate) fn check_sign(sign: Option<Sign>, value: f64) -> Result<(), Error> {
16    let (ok, expected) = match sign {
17        Some(Sign::Positive) => (value > 0.0, "positive number"),
18        Some(Sign::NonNegative) => (value >= 0.0, "non-negative number"),
19        Some(Sign::Negative) => (value < 0.0, "negative number"),
20        Some(Sign::NonPositive) => (value <= 0.0, "non-positive number"),
21        None => (true, ""),
22    };
23    if ok {
24        Ok(())
25    } else {
26        Err(Error::new(ErrorKind::Format { expected }))
27    }
28}
29
30/// (`number` feature) Accepts either an integer or a float, **without converting** between them.
31///
32/// Use this when a value may legitimately be whole or fractional and you want to keep its
33/// original type. Optional inclusive bounds and sign constraints compare the value
34/// numerically but never rewrite it.
35#[derive(Debug, Clone, Default)]
36pub struct Number {
37    meta: Meta,
38    min: Option<f64>,
39    max: Option<f64>,
40    sign: Option<Sign>,
41}
42
43impl Number {
44    /// Attach human-facing metadata (name, description, examples, default, output conversion).
45    pub fn with_meta(mut self, meta: Meta) -> Self {
46        self.meta = meta;
47        self
48    }
49
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    pub fn min(mut self, min: f64) -> Self {
55        self.min = Some(min);
56        self
57    }
58
59    pub fn max(mut self, max: f64) -> Self {
60        self.max = Some(max);
61        self
62    }
63
64    pub fn range(mut self, start: f64, end: f64) -> Self {
65        self.min = Some(start);
66        self.max = Some(end);
67        self
68    }
69
70    /// Require the value to be strictly greater than zero.
71    pub fn positive(mut self) -> Self {
72        self.sign = Some(Sign::Positive);
73        self
74    }
75
76    /// Require the value to be greater than or equal to zero.
77    pub fn non_negative(mut self) -> Self {
78        self.sign = Some(Sign::NonNegative);
79        self
80    }
81
82    /// Require the value to be strictly less than zero.
83    pub fn negative(mut self) -> Self {
84        self.sign = Some(Sign::Negative);
85        self
86    }
87
88    /// Require the value to be less than or equal to zero.
89    pub fn non_positive(mut self) -> Self {
90        self.sign = Some(Sign::NonPositive);
91        self
92    }
93}
94
95impl Validator for Number {
96    fn meta(&self) -> &Meta {
97        &self.meta
98    }
99
100    fn meta_mut(&mut self) -> &mut Meta {
101        &mut self.meta
102    }
103
104    fn check(&self, value: &mut Value) -> Result<(), Error> {
105        let number = match value {
106            Value::Int(number) => *number as f64,
107            Value::Float(number) => *number,
108            _ => return Err(Error::new(ErrorKind::Format { expected: "number" })),
109        };
110
111        if let Some(min) = self.min
112            && number < min
113        {
114            return Err(Error::new(ErrorKind::BelowMin {
115                value: number.to_string(),
116                min: min.to_string(),
117            }));
118        }
119        if let Some(max) = self.max
120            && number > max
121        {
122            return Err(Error::new(ErrorKind::AboveMax {
123                value: number.to_string(),
124                max: max.to_string(),
125            }));
126        }
127
128        check_sign(self.sign, number)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn accepts_int_and_float_without_converting() {
138        let mut int_value = Value::Int(3);
139        Number::new().validate(&mut int_value).unwrap();
140        assert_eq!(int_value, Value::Int(3));
141
142        let mut float_value = Value::Float(3.5);
143        Number::new().validate(&mut float_value).unwrap();
144        assert_eq!(float_value, Value::Float(3.5));
145    }
146
147    #[test]
148    fn rejects_non_numbers() {
149        assert!(
150            Number::new()
151                .validate(&mut Value::String("3".into()))
152                .is_err()
153        );
154    }
155
156    #[test]
157    fn bounds_and_sign() {
158        assert!(
159            Number::new()
160                .range(0.0, 10.0)
161                .validate(&mut Value::Int(5))
162                .is_ok()
163        );
164        assert!(
165            Number::new()
166                .range(0.0, 10.0)
167                .validate(&mut Value::Float(11.0))
168                .is_err()
169        );
170        assert!(
171            Number::new()
172                .positive()
173                .validate(&mut Value::Float(0.0))
174                .is_err()
175        );
176        assert!(
177            Number::new()
178                .non_negative()
179                .validate(&mut Value::Int(0))
180                .is_ok()
181        );
182    }
183}