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