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::utils::humanize_yaml_data;
15use crate::validation::Context;
16use crate::validation::Validator;
17
18#[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}