yaml_schema/schemas/
integer.rs1use 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#[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}