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