Skip to main content

yaml_schema/schemas/
integer.rs

1use log::debug;
2use saphyr::AnnotatedMapping;
3use saphyr::MarkedYaml;
4use saphyr::Scalar;
5use saphyr::YamlData;
6
7use crate::Number;
8use crate::Result;
9use crate::schemas::NumericBounds;
10use crate::utils::format_marker;
11use crate::validation::Context;
12use crate::validation::Validator;
13
14/// An integer schema
15#[derive(Debug, Default, PartialEq)]
16pub struct IntegerSchema {
17    pub bounds: NumericBounds,
18}
19
20impl TryFrom<&MarkedYaml<'_>> for IntegerSchema {
21    type Error = crate::Error;
22
23    fn try_from(value: &MarkedYaml) -> Result<IntegerSchema> {
24        if let YamlData::Mapping(mapping) = &value.data {
25            Ok(IntegerSchema::try_from(mapping)?)
26        } else {
27            Err(expected_mapping!(value))
28        }
29    }
30}
31
32impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for IntegerSchema {
33    type Error = crate::Error;
34
35    fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
36        let mut schema = IntegerSchema::default();
37        for (key, value) in mapping.iter() {
38            if let YamlData::Value(Scalar::String(key)) = &key.data {
39                match key.as_ref() {
40                    "minimum" => {
41                        schema.bounds.minimum = Some(value.try_into()?);
42                    }
43                    "maximum" => {
44                        schema.bounds.maximum = Some(value.try_into()?);
45                    }
46                    "exclusiveMinimum" => {
47                        schema.bounds.exclusive_minimum = Some(value.try_into()?);
48                    }
49                    "exclusiveMaximum" => {
50                        schema.bounds.exclusive_maximum = Some(value.try_into()?);
51                    }
52                    "multipleOf" => {
53                        schema.bounds.multiple_of = Some(value.try_into()?);
54                    }
55                    _ => {
56                        debug!("Unsupported key for `type: integer`: {}", key);
57                    }
58                }
59            } else {
60                return Err(expected_scalar!(
61                    "{} Expected string key, got {:?}",
62                    format_marker(&key.span.start),
63                    key
64                ));
65            }
66        }
67        Ok(schema)
68    }
69}
70
71impl std::fmt::Display for IntegerSchema {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        write!(f, "Integer {self:?}")
74    }
75}
76
77impl Validator for IntegerSchema {
78    fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
79        let data = &value.data;
80        if let saphyr::YamlData::Value(scalar) = data {
81            if let saphyr::Scalar::Integer(i) = scalar {
82                self.bounds.validate(context, value, Number::Integer(*i));
83            } else if let saphyr::Scalar::FloatingPoint(o) = scalar {
84                let f = o.into_inner();
85                if f.fract() == 0.0 {
86                    self.bounds
87                        .validate(context, value, Number::Integer(f as i64));
88                } else {
89                    context.add_error(value, format!("Expected an integer, but got: {data:?}"));
90                }
91            } else {
92                context.add_error(value, format!("Expected a number, but got: {data:?}"));
93            }
94        } else {
95            context.add_error(value, format!("Expected a scalar value, but got: {data:?}"));
96        }
97        if !context.errors.borrow().is_empty() {
98            fail_fast!(context)
99        }
100        Ok(())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use saphyr::LoadableYamlNode;
107
108    use crate::YamlSchema;
109
110    use super::*;
111
112    #[test]
113    fn test_integer_schema_against_string() {
114        let schema = IntegerSchema::default();
115        let context = Context::new(true);
116        let docs = saphyr::MarkedYaml::load_from_str("foo").unwrap();
117        let result = schema.validate(&context, docs.first().unwrap());
118        assert!(result.is_err());
119        let errors = context.errors.borrow();
120        assert!(!errors.is_empty());
121        let first_error = errors.first().unwrap();
122        assert_eq!(
123            first_error.error,
124            "Expected a number, but got: Value(String(\"foo\"))"
125        );
126    }
127
128    #[test]
129    fn test_minimum_float_accepts_value_above() {
130        let schema = IntegerSchema {
131            bounds: NumericBounds {
132                minimum: Some(Number::Float(1.5)),
133                ..Default::default()
134            },
135        };
136        let value = MarkedYaml::value_from_str("2");
137        let context = Context::default();
138        schema
139            .validate(&context, &value)
140            .expect("validate() failed!");
141        assert!(!context.has_errors());
142    }
143
144    #[test]
145    fn test_minimum_float_rejects_value_below() {
146        let schema = IntegerSchema {
147            bounds: NumericBounds {
148                minimum: Some(Number::Float(1.5)),
149                ..Default::default()
150            },
151        };
152        let value = MarkedYaml::value_from_str("1");
153        let context = Context::default();
154        schema
155            .validate(&context, &value)
156            .expect("validate() failed!");
157        assert!(context.has_errors());
158    }
159
160    #[test]
161    fn test_maximum_float_accepts_value_below() {
162        let schema = IntegerSchema {
163            bounds: NumericBounds {
164                maximum: Some(Number::Float(10.5)),
165                ..Default::default()
166            },
167        };
168        let value = MarkedYaml::value_from_str("10");
169        let context = Context::default();
170        schema
171            .validate(&context, &value)
172            .expect("validate() failed!");
173        assert!(!context.has_errors());
174    }
175
176    #[test]
177    fn test_maximum_float_rejects_value_above() {
178        let schema = IntegerSchema {
179            bounds: NumericBounds {
180                maximum: Some(Number::Float(10.5)),
181                ..Default::default()
182            },
183        };
184        let value = MarkedYaml::value_from_str("11");
185        let context = Context::default();
186        schema
187            .validate(&context, &value)
188            .expect("validate() failed!");
189        assert!(context.has_errors());
190    }
191
192    #[test]
193    fn test_exclusive_minimum_float_accepts_value_above() {
194        let schema = IntegerSchema {
195            bounds: NumericBounds {
196                exclusive_minimum: Some(Number::Float(1.5)),
197                ..Default::default()
198            },
199        };
200        let value = MarkedYaml::value_from_str("2");
201        let context = Context::default();
202        schema
203            .validate(&context, &value)
204            .expect("validate() failed!");
205        assert!(!context.has_errors());
206    }
207
208    #[test]
209    fn test_exclusive_minimum_float_rejects_value_below() {
210        let schema = IntegerSchema {
211            bounds: NumericBounds {
212                exclusive_minimum: Some(Number::Float(1.5)),
213                ..Default::default()
214            },
215        };
216        let value = MarkedYaml::value_from_str("1");
217        let context = Context::default();
218        schema
219            .validate(&context, &value)
220            .expect("validate() failed!");
221        assert!(context.has_errors());
222    }
223
224    #[test]
225    fn test_exclusive_maximum_float_accepts_value_below() {
226        let schema = IntegerSchema {
227            bounds: NumericBounds {
228                exclusive_maximum: Some(Number::Float(10.5)),
229                ..Default::default()
230            },
231        };
232        let value = MarkedYaml::value_from_str("10");
233        let context = Context::default();
234        schema
235            .validate(&context, &value)
236            .expect("validate() failed!");
237        assert!(!context.has_errors());
238    }
239
240    #[test]
241    fn test_exclusive_maximum_float_rejects_value_above() {
242        let schema = IntegerSchema {
243            bounds: NumericBounds {
244                exclusive_maximum: Some(Number::Float(10.5)),
245                ..Default::default()
246            },
247        };
248        let value = MarkedYaml::value_from_str("11");
249        let context = Context::default();
250        schema
251            .validate(&context, &value)
252            .expect("validate() failed!");
253        assert!(context.has_errors());
254    }
255
256    #[test]
257    fn test_integer_schema_with_description() {
258        let yaml = r#"
259        type: integer
260        description: The description
261        "#;
262        let marked_yaml = MarkedYaml::load_from_str(yaml).unwrap();
263        let integer_schema = YamlSchema::try_from(marked_yaml.first().unwrap()).unwrap();
264        let YamlSchema::Subschema(subschema) = &integer_schema else {
265            panic!("Expected a subschema");
266        };
267        assert_eq!(
268            subschema.metadata_and_annotations.description,
269            Some("The description".to_string())
270        );
271    }
272}