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::validation::Context;
12use crate::validation::Validator;
13
14#[derive(Debug, Default, PartialEq)]
16pub struct IntegerSchema {
17 pub bounds: NumericBounds,
18}
19
20impl TryFrom<&MarkedYaml<'_>> for IntegerSchema {
21 type Error = crate::Error;
22
23 fn try_from(value: &MarkedYaml) -> Result<IntegerSchema> {
24 if let YamlData::Mapping(mapping) = &value.data {
25 Ok(IntegerSchema::try_from(mapping)?)
26 } else {
27 Err(expected_mapping!(value))
28 }
29 }
30}
31
32impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for IntegerSchema {
33 type Error = crate::Error;
34
35 fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
36 let mut schema = IntegerSchema::default();
37 for (key, value) in mapping.iter() {
38 if let YamlData::Value(Scalar::String(key)) = &key.data {
39 match key.as_ref() {
40 "minimum" => {
41 schema.bounds.minimum = Some(value.try_into()?);
42 }
43 "maximum" => {
44 schema.bounds.maximum = Some(value.try_into()?);
45 }
46 "exclusiveMinimum" => {
47 schema.bounds.exclusive_minimum = Some(value.try_into()?);
48 }
49 "exclusiveMaximum" => {
50 schema.bounds.exclusive_maximum = Some(value.try_into()?);
51 }
52 "multipleOf" => {
53 schema.bounds.multiple_of = Some(value.try_into()?);
54 }
55 _ => {
56 debug!("Unsupported key for `type: integer`: {}", key);
57 }
58 }
59 } else {
60 return Err(expected_scalar!(
61 "{} Expected string key, got {:?}",
62 format_marker(&key.span.start),
63 key
64 ));
65 }
66 }
67 Ok(schema)
68 }
69}
70
71impl std::fmt::Display for IntegerSchema {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(f, "Integer {self:?}")
74 }
75}
76
77impl Validator for IntegerSchema {
78 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
79 let data = &value.data;
80 if let saphyr::YamlData::Value(scalar) = data {
81 if let saphyr::Scalar::Integer(i) = scalar {
82 self.bounds.validate(context, value, Number::Integer(*i));
83 } else if let saphyr::Scalar::FloatingPoint(o) = scalar {
84 let f = o.into_inner();
85 if f.fract() == 0.0 {
86 self.bounds
87 .validate(context, value, Number::Integer(f as i64));
88 } else {
89 context.add_error(value, format!("Expected an integer, but got: {data:?}"));
90 }
91 } else {
92 context.add_error(value, format!("Expected a number, but got: {data:?}"));
93 }
94 } else {
95 context.add_error(value, format!("Expected a scalar value, but got: {data:?}"));
96 }
97 if !context.errors.borrow().is_empty() {
98 fail_fast!(context)
99 }
100 Ok(())
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use saphyr::LoadableYamlNode;
107
108 use crate::YamlSchema;
109
110 use super::*;
111
112 #[test]
113 fn test_integer_schema_against_string() {
114 let schema = IntegerSchema::default();
115 let context = Context::new(true);
116 let docs = saphyr::MarkedYaml::load_from_str("foo").unwrap();
117 let result = schema.validate(&context, docs.first().unwrap());
118 assert!(result.is_err());
119 let errors = context.errors.borrow();
120 assert!(!errors.is_empty());
121 let first_error = errors.first().unwrap();
122 assert_eq!(
123 first_error.error,
124 "Expected a number, but got: Value(String(\"foo\"))"
125 );
126 }
127
128 #[test]
129 fn test_minimum_float_accepts_value_above() {
130 let schema = IntegerSchema {
131 bounds: NumericBounds {
132 minimum: Some(Number::Float(1.5)),
133 ..Default::default()
134 },
135 };
136 let value = MarkedYaml::value_from_str("2");
137 let context = Context::default();
138 schema
139 .validate(&context, &value)
140 .expect("validate() failed!");
141 assert!(!context.has_errors());
142 }
143
144 #[test]
145 fn test_minimum_float_rejects_value_below() {
146 let schema = IntegerSchema {
147 bounds: NumericBounds {
148 minimum: Some(Number::Float(1.5)),
149 ..Default::default()
150 },
151 };
152 let value = MarkedYaml::value_from_str("1");
153 let context = Context::default();
154 schema
155 .validate(&context, &value)
156 .expect("validate() failed!");
157 assert!(context.has_errors());
158 }
159
160 #[test]
161 fn test_maximum_float_accepts_value_below() {
162 let schema = IntegerSchema {
163 bounds: NumericBounds {
164 maximum: Some(Number::Float(10.5)),
165 ..Default::default()
166 },
167 };
168 let value = MarkedYaml::value_from_str("10");
169 let context = Context::default();
170 schema
171 .validate(&context, &value)
172 .expect("validate() failed!");
173 assert!(!context.has_errors());
174 }
175
176 #[test]
177 fn test_maximum_float_rejects_value_above() {
178 let schema = IntegerSchema {
179 bounds: NumericBounds {
180 maximum: Some(Number::Float(10.5)),
181 ..Default::default()
182 },
183 };
184 let value = MarkedYaml::value_from_str("11");
185 let context = Context::default();
186 schema
187 .validate(&context, &value)
188 .expect("validate() failed!");
189 assert!(context.has_errors());
190 }
191
192 #[test]
193 fn test_exclusive_minimum_float_accepts_value_above() {
194 let schema = IntegerSchema {
195 bounds: NumericBounds {
196 exclusive_minimum: Some(Number::Float(1.5)),
197 ..Default::default()
198 },
199 };
200 let value = MarkedYaml::value_from_str("2");
201 let context = Context::default();
202 schema
203 .validate(&context, &value)
204 .expect("validate() failed!");
205 assert!(!context.has_errors());
206 }
207
208 #[test]
209 fn test_exclusive_minimum_float_rejects_value_below() {
210 let schema = IntegerSchema {
211 bounds: NumericBounds {
212 exclusive_minimum: Some(Number::Float(1.5)),
213 ..Default::default()
214 },
215 };
216 let value = MarkedYaml::value_from_str("1");
217 let context = Context::default();
218 schema
219 .validate(&context, &value)
220 .expect("validate() failed!");
221 assert!(context.has_errors());
222 }
223
224 #[test]
225 fn test_exclusive_maximum_float_accepts_value_below() {
226 let schema = IntegerSchema {
227 bounds: NumericBounds {
228 exclusive_maximum: Some(Number::Float(10.5)),
229 ..Default::default()
230 },
231 };
232 let value = MarkedYaml::value_from_str("10");
233 let context = Context::default();
234 schema
235 .validate(&context, &value)
236 .expect("validate() failed!");
237 assert!(!context.has_errors());
238 }
239
240 #[test]
241 fn test_exclusive_maximum_float_rejects_value_above() {
242 let schema = IntegerSchema {
243 bounds: NumericBounds {
244 exclusive_maximum: Some(Number::Float(10.5)),
245 ..Default::default()
246 },
247 };
248 let value = MarkedYaml::value_from_str("11");
249 let context = Context::default();
250 schema
251 .validate(&context, &value)
252 .expect("validate() failed!");
253 assert!(context.has_errors());
254 }
255
256 #[test]
257 fn test_integer_schema_with_description() {
258 let yaml = r#"
259 type: integer
260 description: The description
261 "#;
262 let marked_yaml = MarkedYaml::load_from_str(yaml).unwrap();
263 let integer_schema = YamlSchema::try_from(marked_yaml.first().unwrap()).unwrap();
264 let YamlSchema::Subschema(subschema) = &integer_schema else {
265 panic!("Expected a subschema");
266 };
267 assert_eq!(
268 subschema.metadata_and_annotations.description,
269 Some("The description".to_string())
270 );
271 }
272}