yaml_schema/schemas/
if_then_else.rs1use std::fmt::Display;
4
5use log::debug;
6use saphyr::AnnotatedMapping;
7use saphyr::MarkedYaml;
8use saphyr::YamlData;
9
10use crate::Context;
11use crate::Error;
12use crate::Result;
13use crate::Validator;
14use crate::YamlSchema;
15
16#[derive(Debug, PartialEq)]
18pub struct IfThenElseSchema {
19 pub if_schema: Box<YamlSchema>,
20 pub then_schema: Option<Box<YamlSchema>>,
21 pub else_schema: Option<Box<YamlSchema>>,
22}
23
24impl Display for IfThenElseSchema {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "if: {}", self.if_schema)?;
27 if let Some(t) = &self.then_schema {
28 write!(f, ", then: {t}")?;
29 }
30 if let Some(e) = &self.else_schema {
31 write!(f, ", else: {e}")?;
32 }
33 Ok(())
34 }
35}
36
37impl<'r> TryFrom<&MarkedYaml<'r>> for IfThenElseSchema {
38 type Error = crate::Error;
39
40 fn try_from(value: &MarkedYaml<'r>) -> Result<Self> {
41 if let YamlData::Mapping(mapping) = &value.data {
42 IfThenElseSchema::try_from(mapping)
43 } else {
44 Err(expected_mapping!(value))
45 }
46 }
47}
48
49impl<'r> TryFrom<&AnnotatedMapping<'r, MarkedYaml<'r>>> for IfThenElseSchema {
50 type Error = crate::Error;
51
52 fn try_from(mapping: &AnnotatedMapping<'r, MarkedYaml<'r>>) -> crate::Result<Self> {
53 let if_key = MarkedYaml::value_from_str("if");
54 let Some(if_value) = mapping.get(&if_key) else {
55 return Err(generic_error!("No `if` key found for if/then/else"));
56 };
57 let if_schema: YamlSchema = if_value.try_into()?;
58
59 let then_schema = mapping
60 .get(&MarkedYaml::value_from_str("then"))
61 .map(|v| v.try_into())
62 .transpose()?
63 .map(Box::new);
64
65 let else_schema = mapping
66 .get(&MarkedYaml::value_from_str("else"))
67 .map(|v| v.try_into())
68 .transpose()?
69 .map(Box::new);
70
71 Ok(IfThenElseSchema {
72 if_schema: Box::new(if_schema),
73 then_schema,
74 else_schema,
75 })
76 }
77}
78
79impl Validator for IfThenElseSchema {
80 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> crate::Result<()> {
81 debug!(
82 "if/then/else: validating instance against `if` schema: {}",
83 self.if_schema
84 );
85 let if_context = context.get_sub_context_fresh_eval();
86 let if_result = self.if_schema.validate(&if_context, value);
87
88 let if_passed = match if_result {
89 Ok(()) | Err(Error::FailFast) => !if_context.has_errors(),
90 Err(e) => return Err(e),
91 };
92
93 if if_passed {
94 if let (Some(p), Some(f)) = (&context.object_evaluated, &if_context.object_evaluated) {
95 p.extend(&f.snapshot());
96 }
97 if let (Some(pcell), Some(fcell)) =
98 (&context.array_unevaluated, &if_context.array_unevaluated)
99 {
100 let snap = fcell.borrow().clone();
101 pcell.borrow_mut().merge_from(&snap);
102 }
103 if let Some(then_s) = &self.then_schema {
104 then_s.validate(context, value)?;
105 }
106 } else if let Some(else_s) = &self.else_schema {
107 else_s.validate(context, value)?;
108 }
109
110 Ok(())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use saphyr::LoadableYamlNode;
117
118 use crate::Context;
119 use crate::Engine;
120 use crate::Validator;
121 use crate::loader;
122
123 use super::*;
124
125 #[test]
126 fn if_passes_then_enforced() {
127 let root = loader::load_from_str(
128 r#"
129 if:
130 type: integer
131 then:
132 type: integer
133 minimum: 5
134 "#,
135 )
136 .unwrap();
137 let ctx = Context::with_root_schema(&root, false);
138 let v = MarkedYaml::load_from_str("7").unwrap();
139 root.validate(&ctx, v.first().unwrap()).unwrap();
140 assert!(!ctx.has_errors());
141
142 let ctx = Context::with_root_schema(&root, false);
143 let v = MarkedYaml::load_from_str("3").unwrap();
144 root.validate(&ctx, v.first().unwrap()).unwrap();
145 assert!(ctx.has_errors());
146 }
147
148 #[test]
149 fn if_fails_else_enforced() {
150 let root = loader::load_from_str(
151 r#"
152 if:
153 type: integer
154 else:
155 type: string
156 minLength: 2
157 "#,
158 )
159 .unwrap();
160 let ctx = Context::with_root_schema(&root, false);
161 let v = MarkedYaml::load_from_str("\"ab\"").unwrap();
162 root.validate(&ctx, v.first().unwrap()).unwrap();
163 assert!(!ctx.has_errors());
164
165 let ctx = Context::with_root_schema(&root, false);
166 let v = MarkedYaml::load_from_str("\"x\"").unwrap();
167 root.validate(&ctx, v.first().unwrap()).unwrap();
168 assert!(ctx.has_errors());
169 }
170
171 #[test]
172 fn if_fails_no_else_ok() {
173 let root = loader::load_from_str(
174 r#"
175 if:
176 type: string
177 then:
178 minLength: 10
179 "#,
180 )
181 .unwrap();
182 let ctx = Context::with_root_schema(&root, false);
183 let v = MarkedYaml::load_from_str("42").unwrap();
184 root.validate(&ctx, v.first().unwrap()).unwrap();
185 assert!(
186 !ctx.has_errors(),
187 "if failed so then is skipped; instance should be valid"
188 );
189 }
190
191 #[test]
192 fn if_errors_not_reported_on_parent() {
193 let root = loader::load_from_str(
194 r#"
195 if:
196 type: string
197 then:
198 minLength: 100
199 "#,
200 )
201 .unwrap();
202 let ctx = Context::with_root_schema(&root, false);
203 let v = MarkedYaml::load_from_str("42").unwrap();
204 root.validate(&ctx, v.first().unwrap()).unwrap();
205 assert!(
206 !ctx.has_errors(),
207 "failure of `if` must not surface as parent errors"
208 );
209 }
210
211 #[test]
212 fn type_and_conditional_both_apply() {
213 let root = loader::load_from_str(
214 r#"
215 type: integer
216 maximum: 10
217 if:
218 const: 5
219 then:
220 minimum: 0
221 "#,
222 )
223 .unwrap();
224 let ctx = Context::with_root_schema(&root, false);
225 let v = MarkedYaml::load_from_str("20").unwrap();
226 root.validate(&ctx, v.first().unwrap()).unwrap();
227 assert!(
228 ctx.has_errors(),
229 "fails maximum even when if does not match and then is skipped"
230 );
231 }
232
233 #[test]
236 fn json_schema_doc_usa_canada_postal_if_then_else() {
237 let schema = r#"
238type: object
239properties:
240 street_address:
241 type: string
242 country:
243 enum:
244 - United States of America
245 - Canada
246if:
247 type: object
248 properties:
249 country:
250 const: "United States of America"
251then:
252 type: object
253 properties:
254 postal_code:
255 type: string
256 pattern: '^[0-9]{5}(-[0-9]{4})?$'
257else:
258 type: object
259 properties:
260 postal_code:
261 type: string
262 pattern: '^[A-Z][0-9][A-Z] [0-9][A-Z][0-9]$'
263"#;
264 let root = loader::load_from_str(schema).unwrap();
265
266 let ok_usa = r#"
267street_address: "1600 Pennsylvania Avenue NW"
268country: "United States of America"
269postal_code: "20500"
270"#;
271 assert!(!Engine::evaluate(&root, ok_usa, false).unwrap().has_errors());
272
273 let ok_default_us = r#"
274street_address: "1600 Pennsylvania Avenue NW"
275postal_code: "20500"
276"#;
277 assert!(
278 !Engine::evaluate(&root, ok_default_us, false)
279 .unwrap()
280 .has_errors()
281 );
282
283 let ok_ca = r#"
284street_address: "24 Sussex Drive"
285country: Canada
286postal_code: "K1M 1M4"
287"#;
288 assert!(!Engine::evaluate(&root, ok_ca, false).unwrap().has_errors());
289
290 let bad_ca = r#"
291street_address: "24 Sussex Drive"
292country: Canada
293postal_code: "10000"
294"#;
295 assert!(Engine::evaluate(&root, bad_ca, false).unwrap().has_errors());
296
297 let bad_wrong_zip = r#"
298street_address: "1600 Pennsylvania Avenue NW"
299postal_code: "K1M 1M4"
300"#;
301 assert!(
302 Engine::evaluate(&root, bad_wrong_zip, false)
303 .unwrap()
304 .has_errors()
305 );
306 }
307
308 #[test]
309 fn json_schema_doc_allof_three_countries_postal() {
310 let schema = r#"
311type: object
312properties:
313 street_address:
314 type: string
315 country:
316 enum:
317 - United States of America
318 - Canada
319 - Netherlands
320allOf:
321 - if:
322 type: object
323 properties:
324 country:
325 const: "United States of America"
326 then:
327 type: object
328 properties:
329 postal_code:
330 type: string
331 pattern: '^[0-9]{5}(-[0-9]{4})?$'
332 - if:
333 type: object
334 properties:
335 country:
336 const: Canada
337 required:
338 - country
339 then:
340 type: object
341 properties:
342 postal_code:
343 type: string
344 pattern: '^[A-Z][0-9][A-Z] [0-9][A-Z][0-9]$'
345 - if:
346 type: object
347 properties:
348 country:
349 const: Netherlands
350 required:
351 - country
352 then:
353 type: object
354 properties:
355 postal_code:
356 type: string
357 pattern: '^[0-9]{4} [A-Z]{2}$'
358"#;
359 let root = loader::load_from_str(schema).unwrap();
360
361 let ok_usa = r#"
362street_address: "1600 Pennsylvania Avenue NW"
363country: "United States of America"
364postal_code: "20500"
365"#;
366 assert!(!Engine::evaluate(&root, ok_usa, false).unwrap().has_errors());
367
368 let ok_default_us = r#"
369street_address: "1600 Pennsylvania Avenue NW"
370postal_code: "20500"
371"#;
372 assert!(
373 !Engine::evaluate(&root, ok_default_us, false)
374 .unwrap()
375 .has_errors()
376 );
377
378 let ok_ca = r#"
379street_address: "24 Sussex Drive"
380country: Canada
381postal_code: "K1M 1M4"
382"#;
383 assert!(!Engine::evaluate(&root, ok_ca, false).unwrap().has_errors());
384
385 let nl = r#"
386street_address: "Adriaan Goekooplaan"
387country: Netherlands
388postal_code: "2517 JX"
389"#;
390 assert!(!Engine::evaluate(&root, nl, false).unwrap().has_errors());
391
392 let bad_ca = r#"
393street_address: "24 Sussex Drive"
394country: Canada
395postal_code: "10000"
396"#;
397 assert!(Engine::evaluate(&root, bad_ca, false).unwrap().has_errors());
398
399 let bad_wrong_zip = r#"
400street_address: "1600 Pennsylvania Avenue NW"
401postal_code: "K1M 1M4"
402"#;
403 assert!(
404 Engine::evaluate(&root, bad_wrong_zip, false)
405 .unwrap()
406 .has_errors()
407 );
408 }
409}