Skip to main content

tanzim_validate/
float.rs

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