Skip to main content

yaml_schema/schemas/
number.rs

1use std::collections::HashMap;
2
3use log::debug;
4use saphyr::AnnotatedMapping;
5use saphyr::MarkedYaml;
6use saphyr::Scalar;
7use saphyr::YamlData;
8
9use crate::Number;
10use crate::Result;
11use crate::schemas::NumericBounds;
12use crate::utils::format_hash_map;
13use crate::utils::format_marker;
14use crate::validation::Context;
15use crate::validation::Validator;
16
17/// A number schema
18#[derive(Default, PartialEq)]
19pub struct NumberSchema {
20    pub bounds: NumericBounds,
21}
22
23impl Validator for NumberSchema {
24    fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
25        debug!("[NumberSchema#validate] self: {self:?}");
26        let data = &value.data;
27        debug!("[NumberSchema#validate] data: {data:?}");
28        if let YamlData::Value(scalar) = data {
29            if let Scalar::Integer(i) = scalar {
30                self.bounds.validate(context, value, Number::Integer(*i));
31            } else if let Scalar::FloatingPoint(ordered_float) = scalar {
32                self.bounds
33                    .validate(context, value, Number::Float(ordered_float.into_inner()));
34            } else {
35                context.add_error(value, format!("Expected a number, but got: {data:?}"));
36            }
37        } else {
38            context.add_error(value, format!("Expected a scalar value, but got: {data:?}"));
39        }
40        if context.has_errors() {
41            fail_fast!(context)
42        }
43        Ok(())
44    }
45}
46
47impl TryFrom<&MarkedYaml<'_>> for NumberSchema {
48    type Error = crate::Error;
49
50    fn try_from(value: &MarkedYaml) -> Result<NumberSchema> {
51        if let YamlData::Mapping(mapping) = &value.data {
52            Ok(NumberSchema::try_from(mapping)?)
53        } else {
54            Err(expected_mapping!(value))
55        }
56    }
57}
58
59impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for NumberSchema {
60    type Error = crate::Error;
61
62    fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
63        let mut schema = NumberSchema::default();
64        for (key, value) in mapping.iter() {
65            if let YamlData::Value(Scalar::String(key)) = &key.data {
66                match key.as_ref() {
67                    "minimum" => {
68                        schema.bounds.minimum = Some(value.try_into()?);
69                    }
70                    "maximum" => {
71                        schema.bounds.maximum = Some(value.try_into()?);
72                    }
73                    "exclusiveMinimum" => {
74                        schema.bounds.exclusive_minimum = Some(value.try_into()?);
75                    }
76                    "exclusiveMaximum" => {
77                        schema.bounds.exclusive_maximum = Some(value.try_into()?);
78                    }
79                    "multipleOf" => {
80                        schema.bounds.multiple_of = Some(value.try_into()?);
81                    }
82                    "type" => {
83                        if let YamlData::Value(Scalar::String(s)) = &value.data {
84                            if s != "number" {
85                                return Err(unsupported_type!(
86                                    "Expected type: number, but got: {}",
87                                    s
88                                ));
89                            }
90                        } else if let YamlData::Sequence(values) = &value.data {
91                            if !values
92                                .iter()
93                                .any(|v| v.data == MarkedYaml::value_from_str("number").data)
94                            {
95                                return Err(unsupported_type!(
96                                    "Expected type: number, but got: {:?}",
97                                    value
98                                ));
99                            }
100                        } else {
101                            return Err(expected_type_is_string!(value));
102                        }
103                    }
104                    _ => {
105                        debug!("Unsupported key for type: number: {}", key);
106                    }
107                }
108            } else {
109                return Err(expected_scalar!(
110                    "{} Expected string key, got {:?}",
111                    format_marker(&key.span.start),
112                    key
113                ));
114            }
115        }
116        Ok(schema)
117    }
118}
119
120impl std::fmt::Display for NumberSchema {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "Number {self:?}")
123    }
124}
125
126impl std::fmt::Debug for NumberSchema {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        let mut h = HashMap::new();
129        if let Some(minimum) = self.bounds.minimum {
130            h.insert("minimum".to_string(), minimum.to_string());
131        }
132        if let Some(maximum) = self.bounds.maximum {
133            h.insert("maximum".to_string(), maximum.to_string());
134        }
135        if let Some(exclusive_minimum) = self.bounds.exclusive_minimum {
136            h.insert(
137                "exclusiveMinimum".to_string(),
138                exclusive_minimum.to_string(),
139            );
140        }
141        if let Some(exclusive_maximum) = self.bounds.exclusive_maximum {
142            h.insert(
143                "exclusiveMaximum".to_string(),
144                exclusive_maximum.to_string(),
145            );
146        }
147        if let Some(multiple_of) = self.bounds.multiple_of {
148            h.insert("multipleOf".to_string(), multiple_of.to_string());
149        }
150        write!(f, "Number {}", format_hash_map(&h))
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_number_schema_debug() {
160        let number_schema = NumberSchema {
161            bounds: NumericBounds {
162                minimum: Some(Number::Integer(1)),
163                ..Default::default()
164            },
165        };
166        let marked_yaml = MarkedYaml::value_from_str("1");
167        let context = Context::default();
168        number_schema
169            .validate(&context, &marked_yaml)
170            .expect("validate() failed!");
171        assert!(!context.has_errors());
172    }
173
174    #[test]
175    fn test_number_schema_should_not_accept_boolean() {
176        let number_schema = NumberSchema::default();
177        let marked_yaml = MarkedYaml::value_from_str("true");
178        assert!(marked_yaml.data.is_boolean());
179        let context = Context::default();
180        number_schema
181            .validate(&context, &marked_yaml)
182            .expect("validate() failed!");
183        assert!(context.has_errors());
184    }
185
186    #[test]
187    fn test_exclusive_minimum_float_accepts_value_above() {
188        let schema = NumberSchema {
189            bounds: NumericBounds {
190                exclusive_minimum: Some(Number::Float(1.5)),
191                ..Default::default()
192            },
193        };
194        let value = MarkedYaml::value_from_str("1.6");
195        let context = Context::default();
196        schema
197            .validate(&context, &value)
198            .expect("validate() failed!");
199        assert!(!context.has_errors());
200    }
201
202    #[test]
203    fn test_exclusive_minimum_float_rejects_equal_value() {
204        let schema = NumberSchema {
205            bounds: NumericBounds {
206                exclusive_minimum: Some(Number::Float(1.5)),
207                ..Default::default()
208            },
209        };
210        let value = MarkedYaml::value_from_str("1.5");
211        let context = Context::default();
212        schema
213            .validate(&context, &value)
214            .expect("validate() failed!");
215        assert!(context.has_errors());
216    }
217
218    #[test]
219    fn test_exclusive_minimum_float_rejects_value_below() {
220        let schema = NumberSchema {
221            bounds: NumericBounds {
222                exclusive_minimum: Some(Number::Float(1.5)),
223                ..Default::default()
224            },
225        };
226        let value = MarkedYaml::value_from_str("1.4");
227        let context = Context::default();
228        schema
229            .validate(&context, &value)
230            .expect("validate() failed!");
231        assert!(context.has_errors());
232    }
233
234    #[test]
235    fn test_exclusive_maximum_float_accepts_value_below() {
236        let schema = NumberSchema {
237            bounds: NumericBounds {
238                exclusive_maximum: Some(Number::Float(10.5)),
239                ..Default::default()
240            },
241        };
242        let value = MarkedYaml::value_from_str("10.4");
243        let context = Context::default();
244        schema
245            .validate(&context, &value)
246            .expect("validate() failed!");
247        assert!(!context.has_errors());
248    }
249
250    #[test]
251    fn test_exclusive_maximum_float_rejects_equal_value() {
252        let schema = NumberSchema {
253            bounds: NumericBounds {
254                exclusive_maximum: Some(Number::Float(10.5)),
255                ..Default::default()
256            },
257        };
258        let value = MarkedYaml::value_from_str("10.5");
259        let context = Context::default();
260        schema
261            .validate(&context, &value)
262            .expect("validate() failed!");
263        assert!(context.has_errors());
264    }
265
266    #[test]
267    fn test_exclusive_maximum_float_rejects_value_above() {
268        let schema = NumberSchema {
269            bounds: NumericBounds {
270                exclusive_maximum: Some(Number::Float(10.5)),
271                ..Default::default()
272            },
273        };
274        let value = MarkedYaml::value_from_str("10.6");
275        let context = Context::default();
276        schema
277            .validate(&context, &value)
278            .expect("validate() failed!");
279        assert!(context.has_errors());
280    }
281
282    #[test]
283    fn test_exclusive_minimum_int_boundary_with_float_value() {
284        let schema = NumberSchema {
285            bounds: NumericBounds {
286                exclusive_minimum: Some(Number::Integer(5)),
287                ..Default::default()
288            },
289        };
290        let value = MarkedYaml::value_from_str("5.0");
291        let context = Context::default();
292        schema
293            .validate(&context, &value)
294            .expect("validate() failed!");
295        assert!(context.has_errors());
296    }
297
298    #[test]
299    fn test_exclusive_maximum_int_boundary_with_float_value() {
300        let schema = NumberSchema {
301            bounds: NumericBounds {
302                exclusive_maximum: Some(Number::Integer(5)),
303                ..Default::default()
304            },
305        };
306        let value = MarkedYaml::value_from_str("5.0");
307        let context = Context::default();
308        schema
309            .validate(&context, &value)
310            .expect("validate() failed!");
311        assert!(context.has_errors());
312    }
313
314    #[test]
315    fn test_exclusive_min_and_max_float_accepts_value_in_range() {
316        let schema = NumberSchema {
317            bounds: NumericBounds {
318                exclusive_minimum: Some(Number::Float(1.0)),
319                exclusive_maximum: Some(Number::Float(10.0)),
320                ..Default::default()
321            },
322        };
323        let value = MarkedYaml::value_from_str("5.5");
324        let context = Context::default();
325        schema
326            .validate(&context, &value)
327            .expect("validate() failed!");
328        assert!(!context.has_errors());
329    }
330
331    #[test]
332    fn test_exclusive_min_and_max_float_rejects_lower_boundary() {
333        let schema = NumberSchema {
334            bounds: NumericBounds {
335                exclusive_minimum: Some(Number::Float(1.0)),
336                exclusive_maximum: Some(Number::Float(10.0)),
337                ..Default::default()
338            },
339        };
340        let value = MarkedYaml::value_from_str("1.0");
341        let context = Context::default();
342        schema
343            .validate(&context, &value)
344            .expect("validate() failed!");
345        assert!(context.has_errors());
346    }
347
348    #[test]
349    fn test_exclusive_min_and_max_float_rejects_upper_boundary() {
350        let schema = NumberSchema {
351            bounds: NumericBounds {
352                exclusive_minimum: Some(Number::Float(1.0)),
353                exclusive_maximum: Some(Number::Float(10.0)),
354                ..Default::default()
355            },
356        };
357        let value = MarkedYaml::value_from_str("10.0");
358        let context = Context::default();
359        schema
360            .validate(&context, &value)
361            .expect("validate() failed!");
362        assert!(context.has_errors());
363    }
364}