yaml_schema/schemas/
integer.rs1use crate::ConstValue;
2use crate::Number;
3use crate::Result;
4use crate::schemas::BaseSchema;
5use crate::schemas::SchemaMetadata;
6use crate::utils::format_marker;
7use crate::validation::Context;
8use crate::validation::Validator;
9use saphyr::AnnotatedMapping;
10use saphyr::MarkedYaml;
11use saphyr::Scalar;
12use saphyr::YamlData;
13use std::cmp::Ordering;
14
15#[derive(Debug, Default, PartialEq)]
17pub struct IntegerSchema {
18 pub base: BaseSchema,
19 pub minimum: Option<Number>,
20 pub maximum: Option<Number>,
21 pub exclusive_minimum: Option<Number>,
22 pub exclusive_maximum: Option<Number>,
23 pub multiple_of: Option<Number>,
24}
25
26impl SchemaMetadata for IntegerSchema {
27 fn get_accepted_keys() -> &'static [&'static str] {
28 &[
29 "minimum",
30 "maximum",
31 "exclusiveMinimum",
32 "exclusiveMaximum",
33 "multipleOf",
34 ]
35 }
36}
37
38impl TryFrom<&MarkedYaml<'_>> for IntegerSchema {
39 type Error = crate::Error;
40
41 fn try_from(value: &MarkedYaml) -> Result<IntegerSchema> {
42 if let YamlData::Mapping(mapping) = &value.data {
43 Ok(IntegerSchema::try_from(mapping)?)
44 } else {
45 Err(expected_mapping!(value))
46 }
47 }
48}
49
50impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for IntegerSchema {
51 type Error = crate::Error;
52
53 fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
54 let mut integer_schema = IntegerSchema::from_base(BaseSchema::try_from(mapping)?);
55 for (key, value) in mapping.iter() {
56 if let YamlData::Value(Scalar::String(key)) = &key.data {
57 if integer_schema.base.handle_key_value(key, value)?.is_none() {
58 match key.as_ref() {
59 "minimum" => {
60 integer_schema.minimum = Some(value.try_into()?);
61 }
62 "maximum" => {
63 integer_schema.maximum = Some(value.try_into()?);
64 }
65 "exclusiveMinimum" => {
66 integer_schema.exclusive_minimum = Some(value.try_into()?);
67 }
68 "exclusiveMaximum" => {
69 integer_schema.exclusive_maximum = Some(value.try_into()?);
70 }
71 "multipleOf" => {
72 integer_schema.multiple_of = Some(value.try_into()?);
73 }
74 "type" => {
76 if let YamlData::Value(Scalar::String(s)) = &value.data {
77 if s != "integer" {
78 return Err(unsupported_type!(
79 "Expected type: integer, but got: {}",
80 s
81 ));
82 }
83 } else {
84 return Err(expected_type_is_string!(value));
85 }
86 }
87 _ => unimplemented!("Unsupported key for type: integer: {}", key),
88 }
89 }
90 } else {
91 return Err(generic_error!(
92 "{} Expected string key, got {:?}",
93 format_marker(&key.span.start),
94 key
95 ));
96 }
97 }
98 Ok(integer_schema)
99 }
100}
101
102impl std::fmt::Display for IntegerSchema {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 write!(f, "Integer {self:?}")
105 }
106}
107
108impl Validator for IntegerSchema {
109 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
110 let data = &value.data;
111 let enum_values = self.base.r#enum.as_ref().map(|r#enum| {
112 r#enum
113 .iter()
114 .filter_map(|v| {
115 if let ConstValue::Number(Number::Integer(i)) = v {
116 Some(*i)
117 } else {
118 None
119 }
120 })
121 .collect::<Vec<i64>>()
122 });
123 if let saphyr::YamlData::Value(scalar) = data {
124 if let saphyr::Scalar::Integer(i) = scalar {
125 self.validate_integer(context, &enum_values, value, *i);
126 } else if let saphyr::Scalar::FloatingPoint(o) = scalar {
127 let f = o.into_inner();
128 if f.fract() == 0.0 {
129 self.validate_integer(context, &enum_values, value, f as i64);
130 } else {
131 context.add_error(value, format!("Expected an integer, but got: {data:?}"));
132 }
133 } else {
134 context.add_error(value, format!("Expected a number, but got: {data:?}"));
135 }
136 } else {
137 context.add_error(value, format!("Expected a scalar value, but got: {data:?}"));
138 }
139 if !context.errors.borrow().is_empty() {
140 fail_fast!(context)
141 }
142 Ok(())
143 }
144}
145
146impl IntegerSchema {
147 pub fn from_base(base: BaseSchema) -> Self {
148 Self {
149 base,
150 ..Default::default()
151 }
152 }
153
154 fn validate_integer(
155 &self,
156 context: &Context,
157 enum_values: &Option<Vec<i64>>,
158 value: &MarkedYaml,
159 i: i64,
160 ) {
161 if let Some(exclusive_min) = self.exclusive_minimum {
162 match exclusive_min {
163 Number::Integer(exclusive_min) => {
164 if i <= exclusive_min {
165 context.add_error(
166 value,
167 format!("Number must be greater than {exclusive_min}"),
168 );
169 }
170 }
171 Number::Float(exclusive_min) => {
172 if (i as f64).partial_cmp(&exclusive_min) != Some(Ordering::Greater) {
173 context.add_error(
174 value,
175 format!("Number must be greater than {exclusive_min}"),
176 );
177 }
178 }
179 }
180 } else if let Some(minimum) = self.minimum {
181 match minimum {
182 Number::Integer(min) => {
183 if i <= min {
184 context.add_error(
185 value,
186 format!("Number must be greater than or equal to {min}"),
187 );
188 }
189 }
190 Number::Float(min) => {
191 let cmp = (i as f64).partial_cmp(&min);
192 if cmp != Some(Ordering::Less) && cmp != Some(Ordering::Equal) {
193 context.add_error(
194 value,
195 format!("Number must be greater than or equal to {min}"),
196 );
197 }
198 }
199 }
200 }
201
202 if let Some(exclusive_max) = self.exclusive_maximum {
203 match exclusive_max {
204 Number::Integer(exclusive_max) => {
205 if i >= exclusive_max {
206 context.add_error(
207 value,
208 format!("Number must be less than than {exclusive_max}"),
209 );
210 }
211 }
212 Number::Float(exclusive_max) => {
213 if (i as f64).partial_cmp(&exclusive_max) != Some(Ordering::Less) {
214 context.add_error(
215 value,
216 format!("Number must be less than than {exclusive_max}"),
217 );
218 }
219 }
220 }
221 } else if let Some(maximum) = self.maximum {
222 match maximum {
223 Number::Integer(max) => {
224 if i >= max {
225 context.add_error(
226 value,
227 format!("Number must be less than or equal to {max}"),
228 );
229 }
230 }
231 Number::Float(max) => {
232 let cmp = (i as f64).partial_cmp(&max);
233 if cmp != Some(Ordering::Greater) && cmp != Some(Ordering::Equal) {
234 context.add_error(
235 value,
236 format!("Number must be less than or equal to {max}"),
237 );
238 }
239 }
240 }
241 }
242
243 if let Some(multiple_of) = self.multiple_of {
244 match multiple_of {
245 Number::Integer(multiple) => {
246 if i % multiple != 0 {
247 context
248 .add_error(value, format!("Number is not a multiple of {multiple}!"));
249 }
250 }
251 Number::Float(multiple) => {
252 if (i as f64) % multiple != 0.0 {
253 context
254 .add_error(value, format!("Number is not a multiple of {multiple}!"));
255 }
256 }
257 }
258 }
259 if let Some(enum_values) = enum_values
260 && !enum_values.contains(&i)
261 {
262 context.add_error(value, format!("Number is not in enum: {enum_values:?}"));
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use saphyr::LoadableYamlNode;
270
271 use super::*;
272
273 #[test]
274 fn test_integer_schema_against_string() {
275 let schema = IntegerSchema::default();
276 let context = Context::new(true);
277 let docs = saphyr::MarkedYaml::load_from_str("foo").unwrap();
278 let result = schema.validate(&context, docs.first().unwrap());
279 assert!(result.is_err());
280 let errors = context.errors.borrow();
281 assert!(!errors.is_empty());
282 let first_error = errors.first().unwrap();
283 assert_eq!(
284 first_error.error,
285 "Expected a number, but got: Value(String(\"foo\"))"
286 );
287 }
288
289 #[test]
290 fn test_integer_schema_with_description() {
291 let yaml = r#"
292 type: integer
293 description: The description
294 "#;
295 let marked_yaml = MarkedYaml::load_from_str(yaml).unwrap();
296 let integer_schema = IntegerSchema::try_from(marked_yaml.first().unwrap()).unwrap();
297 assert_eq!(
298 integer_schema.base.description,
299 Some("The description".to_string())
300 );
301 }
302}