yaml_schema/schemas/
number.rs1use std::cmp::Ordering;
2
3use log::debug;
4use saphyr::AnnotatedMapping;
5use saphyr::MarkedYaml;
6use saphyr::Scalar;
7use saphyr::YamlData;
8
9use crate::ConstValue;
10use crate::Number;
11use crate::Result;
12use crate::schemas::BaseSchema;
13use crate::schemas::SchemaMetadata;
14use crate::utils::format_hash_map;
15use crate::utils::format_marker;
16use crate::validation::Context;
17use crate::validation::Validator;
18
19#[derive(Default, PartialEq)]
21pub struct NumberSchema {
22 pub base: BaseSchema,
23 pub minimum: Option<Number>,
24 pub maximum: Option<Number>,
25 pub exclusive_minimum: Option<Number>,
26 pub exclusive_maximum: Option<Number>,
27 pub multiple_of: Option<Number>,
28}
29
30impl SchemaMetadata for NumberSchema {
31 fn get_accepted_keys() -> &'static [&'static str] {
32 &[
33 "minimum",
34 "maximum",
35 "exclusiveMinimum",
36 "exclusiveMaximum",
37 "multipleOf",
38 ]
39 }
40}
41
42impl Validator for NumberSchema {
43 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
44 debug!("[NumberSchema#validate] self: {self:?}");
45 let data = &value.data;
46 debug!("[NumberSchema#validate] data: {data:?}");
47 if let YamlData::Value(scalar) = data {
48 if let Scalar::Integer(i) = scalar {
49 let enum_values = self.base.r#enum.as_ref().map(|r#enum| {
50 r#enum
51 .iter()
52 .filter_map(|v| {
53 if let ConstValue::Number(Number::Integer(i)) = v {
54 Some(*i)
55 } else {
56 None
57 }
58 })
59 .collect::<Vec<i64>>()
60 });
61 self.validate_number_i64(context, &enum_values, value, *i)
62 } else if let Scalar::FloatingPoint(o) = scalar {
63 let enum_values = self.base.r#enum.as_ref().map(|r#enum| {
64 r#enum
65 .iter()
66 .filter_map(|v| {
67 if let ConstValue::Number(Number::Float(f)) = v {
68 Some(*f)
69 } else {
70 None
71 }
72 })
73 .collect::<Vec<f64>>()
74 });
75 self.validate_number_f64(context, &enum_values, value, o.into_inner())
76 } else {
77 context.add_error(value, format!("Expected a number, but got: {data:?}"));
78 }
79 } else {
80 context.add_error(value, format!("Expected a scalar value, but got: {data:?}"));
81 }
82 if !context.errors.borrow().is_empty() {
83 fail_fast!(context)
84 }
85 Ok(())
86 }
87}
88
89impl NumberSchema {
90 pub fn from_base(base: BaseSchema) -> Self {
91 Self {
92 base,
93 ..Default::default()
94 }
95 }
96
97 fn validate_number_i64(
99 &self,
100 context: &Context,
101 enum_values: &Option<Vec<i64>>,
102 value: &MarkedYaml,
103 i: i64,
104 ) {
105 debug!("[NumberSchema#validate_number_i64] self: {self:?}");
106 debug!("[NumberSchema#validate_number_i64] enum_values: {enum_values:?}");
107 debug!(
108 "[NumberSchema#validate_number_i64] value: {:?}",
109 &value.data
110 );
111 debug!("[NumberSchema#validate_number_i64] i: {i}");
112 if let Some(exclusive_min) = self.exclusive_minimum {
113 match exclusive_min {
114 Number::Integer(exclusive_min) => {
115 if i <= exclusive_min {
116 context.add_error(
117 value,
118 format!("Number must be greater than {exclusive_min}"),
119 );
120 }
121 }
122 Number::Float(exclusive_min) => {
123 if (i as f64).partial_cmp(&exclusive_min) != Some(Ordering::Greater) {
124 context.add_error(
125 value,
126 format!("Number must be greater than {exclusive_min}"),
127 );
128 }
129 }
130 }
131 } else if let Some(minimum) = self.minimum {
132 match minimum {
133 Number::Integer(min) => {
134 if i < min {
135 context.add_error(
136 value,
137 format!("Number must be greater than or equal to {min}"),
138 );
139 }
140 }
141 Number::Float(min) => {
142 let cmp = min.partial_cmp(&(i as f64));
143 if cmp == Some(Ordering::Less) {
144 context.add_error(
145 value,
146 format!("Number must be greater than or equal to {min}"),
147 );
148 }
149 }
150 }
151 }
152
153 if let Some(exclusive_max) = self.exclusive_maximum {
154 match exclusive_max {
155 Number::Integer(exclusive_max) => {
156 if i >= exclusive_max {
157 context.add_error(
158 value,
159 format!("Number must be less than than {exclusive_max}"),
160 );
161 }
162 }
163 Number::Float(exclusive_max) => {
164 if (i as f64).partial_cmp(&exclusive_max) != Some(Ordering::Less) {
165 context.add_error(
166 value,
167 format!("Number must be less than than {exclusive_max}"),
168 );
169 }
170 }
171 }
172 } else if let Some(maximum) = self.maximum {
173 match maximum {
174 Number::Integer(max) => {
175 if i >= max {
176 context.add_error(
177 value,
178 format!("Number must be less than or equal to {max}"),
179 );
180 }
181 }
182 Number::Float(max) => {
183 let cmp = (i as f64).partial_cmp(&max);
184 if cmp != Some(Ordering::Greater) && cmp != Some(Ordering::Equal) {
185 context.add_error(
186 value,
187 format!("Number must be less than or equal to {max}"),
188 );
189 }
190 }
191 }
192 }
193
194 if let Some(multiple_of) = self.multiple_of {
195 match multiple_of {
196 Number::Integer(multiple) => {
197 if i % multiple != 0 {
198 context
199 .add_error(value, format!("Number is not a multiple of {multiple}!"));
200 }
201 }
202 Number::Float(multiple) => {
203 if (i as f64) % multiple != 0.0 {
204 context
205 .add_error(value, format!("Number is not a multiple of {multiple}!"));
206 }
207 }
208 }
209 }
210 if let Some(enum_values) = enum_values
211 && !enum_values.contains(&i)
212 {
213 context.add_error(value, format!("Number is not in enum: {enum_values:?}"));
214 }
215 }
216
217 fn validate_number_f64(
218 &self,
219 context: &Context,
220 enum_values: &Option<Vec<f64>>,
221 value: &MarkedYaml,
222 f: f64,
223 ) {
224 if let Some(minimum) = &self.minimum {
225 match minimum {
226 Number::Integer(min) => {
227 if f < *min as f64 {
228 context.add_error(value, "Number is too small!".to_string());
229 }
230 }
231 Number::Float(min) => {
232 if f < *min {
233 context.add_error(value, "Number is too small!".to_string());
234 }
235 }
236 }
237 }
238 if let Some(maximum) = &self.maximum {
239 match maximum {
240 Number::Integer(max) => {
241 if f > *max as f64 {
242 context.add_error(value, "Number is too big!".to_string());
243 }
244 }
245 Number::Float(max) => {
246 if f > *max {
247 context.add_error(value, "Number is too big!".to_string());
248 }
249 }
250 }
251 }
252 if let Some(enum_values) = enum_values
253 && !enum_values.contains(&f)
254 {
255 context.add_error(value, format!("Number is not in enum: {enum_values:?}"));
256 }
257 }
258}
259
260impl TryFrom<&MarkedYaml<'_>> for NumberSchema {
261 type Error = crate::Error;
262
263 fn try_from(value: &MarkedYaml) -> Result<NumberSchema> {
264 if let YamlData::Mapping(mapping) = &value.data {
265 Ok(NumberSchema::try_from(mapping)?)
266 } else {
267 Err(expected_mapping!(value))
268 }
269 }
270}
271
272impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for NumberSchema {
273 type Error = crate::Error;
274
275 fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
276 let mut number_schema = NumberSchema::from_base(BaseSchema::try_from(mapping)?);
277 for (key, value) in mapping.iter() {
278 if let YamlData::Value(Scalar::String(key)) = &key.data {
279 if number_schema.base.handle_key_value(key, value)?.is_none() {
280 match key.as_ref() {
281 "minimum" => {
282 number_schema.minimum = Some(value.try_into()?);
283 }
284 "maximum" => {
285 number_schema.maximum = Some(value.try_into()?);
286 }
287 "exclusiveMinimum" => {
288 number_schema.exclusive_minimum = Some(value.try_into()?);
289 }
290 "exclusiveMaximum" => {
291 number_schema.exclusive_maximum = Some(value.try_into()?);
292 }
293 "multipleOf" => {
294 number_schema.multiple_of = Some(value.try_into()?);
295 }
296 "type" => {
298 if let YamlData::Value(Scalar::String(s)) = &value.data {
299 if s != "number" {
300 return Err(unsupported_type!(
301 "Expected type: number, but got: {}",
302 s
303 ));
304 }
305 } else {
306 return Err(expected_type_is_string!(value));
307 }
308 }
309 _ => {
310 return Err(schema_loading_error!(
311 "Unsupported key for type: number: {}",
312 key
313 ));
314 }
315 }
316 }
317 } else {
318 return Err(expected_scalar!(
319 "{} Expected string key, got {:?}",
320 format_marker(&key.span.start),
321 key
322 ));
323 }
324 }
325 Ok(number_schema)
326 }
327}
328
329impl std::fmt::Display for NumberSchema {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 write!(f, "Number {self:?}")
332 }
333}
334
335impl std::fmt::Debug for NumberSchema {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 let mut h = self.base.as_hash_map();
338 if let Some(minimum) = self.minimum {
339 h.insert("minimum".to_string(), minimum.to_string());
340 }
341 if let Some(maximum) = self.maximum {
342 h.insert("maximum".to_string(), maximum.to_string());
343 }
344 if let Some(exclusive_minimum) = self.exclusive_minimum {
345 h.insert(
346 "exclusiveMinimum".to_string(),
347 exclusive_minimum.to_string(),
348 );
349 }
350 if let Some(exclusive_maximum) = self.exclusive_maximum {
351 h.insert(
352 "exclusiveMaximum".to_string(),
353 exclusive_maximum.to_string(),
354 );
355 }
356 if let Some(multiple_of) = self.multiple_of {
357 h.insert("multipleOf".to_string(), multiple_of.to_string());
358 }
359 write!(f, "Number {}", format_hash_map(&h))
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_number_schema_debug() {
369 let number_schema = NumberSchema {
370 minimum: Some(Number::Integer(1)),
371 ..Default::default()
372 };
373 println!("number_schema: {number_schema:?}");
374 let marked_yaml = MarkedYaml::value_from_str("1");
375 println!("marked_yaml: {marked_yaml:?}");
376 let context = Context::default();
377 number_schema
378 .validate(&context, &marked_yaml)
379 .expect("validate() failed!");
380 println!("context: {context:?}");
381 assert!(!context.has_errors());
382 }
383}