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