yaml_schema/schemas/
number.rs

1use 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/// A number schema
20#[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    // TODO: This duplicates IntegerSchema::validate_integer(), so, find a neat way to dedupe this
98    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                        // Maybe this should be handled by the base schema?
297                        "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}