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
95crate::impl_meta_methods!(Number);
96
97impl Validator for Number {
98    fn meta(&self) -> &Meta {
99        &self.meta
100    }
101
102    fn meta_mut(&mut self) -> &mut Meta {
103        &mut self.meta
104    }
105
106    fn check(&self, value: &mut Value) -> Result<(), Error> {
107        let number = match value {
108            Value::Int(number) => *number as f64,
109            Value::Float(number) => *number,
110            _ => return Err(Error::new(ErrorKind::Format { expected: "number" })),
111        };
112
113        if let Some(min) = self.min
114            && number < min
115        {
116            return Err(Error::new(ErrorKind::BelowMin {
117                value: number.to_string(),
118                min: min.to_string(),
119            }));
120        }
121        if let Some(max) = self.max
122            && number > max
123        {
124            return Err(Error::new(ErrorKind::AboveMax {
125                value: number.to_string(),
126                max: max.to_string(),
127            }));
128        }
129
130        check_sign(self.sign, number)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn accepts_int_and_float_without_converting() {
140        let mut int_value = Value::Int(3);
141        Number::new().validate(&mut int_value).unwrap();
142        assert_eq!(int_value, Value::Int(3));
143
144        let mut float_value = Value::Float(3.5);
145        Number::new().validate(&mut float_value).unwrap();
146        assert_eq!(float_value, Value::Float(3.5));
147    }
148
149    #[test]
150    fn rejects_non_numbers() {
151        assert!(
152            Number::new()
153                .validate(&mut Value::String("3".into()))
154                .is_err()
155        );
156    }
157
158    #[test]
159    fn bounds_and_sign() {
160        assert!(
161            Number::new()
162                .range(0.0, 10.0)
163                .validate(&mut Value::Int(5))
164                .is_ok()
165        );
166        assert!(
167            Number::new()
168                .range(0.0, 10.0)
169                .validate(&mut Value::Float(11.0))
170                .is_err()
171        );
172        assert!(
173            Number::new()
174                .positive()
175                .validate(&mut Value::Float(0.0))
176                .is_err()
177        );
178        assert!(
179            Number::new()
180                .non_negative()
181                .validate(&mut Value::Int(0))
182                .is_ok()
183        );
184    }
185}