yaml_schema/schemas/
one_of.rs1use log::debug;
2use log::error;
3use saphyr::AnnotatedMapping;
4use saphyr::MarkedYaml;
5use saphyr::YamlData;
6
7use crate::Context;
8use crate::Error;
9use crate::Result;
10use crate::Validator;
11use crate::YamlSchema;
12use crate::loader;
13use crate::utils::format_vec;
14use crate::utils::format_yaml_data;
15use crate::validation::ArrayUnevaluatedAnnotations;
16
17#[derive(Debug, Default, PartialEq)]
21pub struct OneOfSchema {
22 pub one_of: Vec<YamlSchema>,
23}
24
25impl std::fmt::Display for OneOfSchema {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 write!(f, "oneOf:{}", format_vec(&self.one_of))
28 }
29}
30
31impl<'r> TryFrom<&MarkedYaml<'r>> for OneOfSchema {
32 type Error = crate::Error;
33
34 fn try_from(value: &MarkedYaml<'r>) -> Result<Self> {
35 if let YamlData::Mapping(mapping) = &value.data {
36 OneOfSchema::try_from(mapping)
37 } else {
38 Err(expected_mapping!(value))
39 }
40 }
41}
42
43impl<'r> TryFrom<&AnnotatedMapping<'r, MarkedYaml<'r>>> for OneOfSchema {
44 type Error = crate::Error;
45
46 fn try_from(mapping: &AnnotatedMapping<'r, MarkedYaml<'r>>) -> Result<Self> {
47 debug!("[OneOfSchema#try_from] mapping: {mapping:?}");
48 match mapping.get(&MarkedYaml::value_from_str("oneOf")) {
49 Some(marked_yaml) => {
50 debug!(
51 "[OneOfSchema#try_from] marked_yaml: {}",
52 format_yaml_data(&marked_yaml.data)
53 );
54 let one_of = loader::load_array_of_schemas_marked(marked_yaml)?;
55 Ok(OneOfSchema { one_of })
56 }
57 None => Err(generic_error!("No `oneOf` key found!")),
58 }
59 }
60}
61
62impl Validator for crate::schemas::OneOfSchema {
63 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
64 let one_of_is_valid = validate_one_of(context, &self.one_of, value)?;
65 if !one_of_is_valid {
66 context.add_error(value, "None of the schemas in `oneOf` matched!");
67 fail_fast!(context);
68 }
69 Ok(())
70 }
71}
72
73pub fn validate_one_of(
74 context: &Context,
75 schemas: &[YamlSchema],
76 value: &saphyr::MarkedYaml,
77) -> Result<bool> {
78 let mut match_count = 0usize;
79 let mut winning_obj = None;
80 let mut winning_arr: Option<ArrayUnevaluatedAnnotations> = None;
81
82 for schema in schemas {
83 debug!(
84 "[OneOf] Validating value: {:?} against schema: {}",
85 &value.data, schema
86 );
87 let sub_context = context.get_sub_context_fresh_eval();
88 let sub_result = schema.validate(&sub_context, value);
89 match sub_result {
90 Ok(()) | Err(Error::FailFast) => {
91 debug!(
92 "[OneOf] sub_context.errors: {}",
93 sub_context.errors.borrow().len()
94 );
95 if sub_context.has_errors() {
96 continue;
97 }
98
99 match_count += 1;
100 if match_count == 1 {
101 winning_obj = sub_context.object_evaluated.as_ref().map(|o| o.snapshot());
102 winning_arr = sub_context
103 .array_unevaluated
104 .as_ref()
105 .map(|a| a.borrow().clone());
106 }
107 }
108 Err(e) => return Err(e),
109 }
110 }
111
112 if match_count > 1 {
113 error!("[OneOf] Value matched multiple schemas in `oneOf`!");
114 context.add_error(value, "Value matched multiple schemas in `oneOf`!");
115 fail_fast!(context);
116 return Ok(false);
117 }
118
119 if match_count == 1 {
120 if let (Some(p), Some(s)) = (&context.object_evaluated, winning_obj) {
121 p.extend(&s);
122 }
123 if let (Some(pcell), Some(snap)) = (&context.array_unevaluated, winning_arr) {
124 pcell.borrow_mut().merge_from(&snap);
125 }
126 }
127
128 debug!("OneOf: match_count: {match_count}");
129 Ok(match_count == 1)
130}
131
132#[cfg(test)]
133mod tests {
134 use saphyr::LoadableYamlNode;
135 use saphyr::MarkedYaml;
136
137 use crate::YamlSchema;
138 use crate::loader;
139 use crate::schemas::SchemaType;
140
141 use super::*;
142
143 #[test]
144 fn test_one_of_schema() {
145 let yaml = r#"
146 oneOf:
147 - type: boolean
148 - type: integer
149 "#;
150 let root_schema = loader::load_from_str(yaml).expect("Failed to load schema");
151 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
152 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
153 };
154 let Some(one_of_schema) = &subschema.one_of else {
155 panic!("Expected Subschema with oneOf, but got: {subschema:?}");
156 };
157
158 if let YamlSchema::Subschema(subschema) = &one_of_schema.one_of[0]
159 && let SchemaType::Single(type_value) = &subschema.r#type
160 {
161 assert_eq!(type_value, "boolean");
162 } else {
163 panic!(
164 "Expected Subschema with type: boolean, but got: {:?}",
165 &one_of_schema.one_of[0]
166 );
167 }
168
169 if let YamlSchema::Subschema(subschema) = &one_of_schema.one_of[1]
170 && let SchemaType::Single(type_value) = &subschema.r#type
171 {
172 assert_eq!(type_value, "integer");
173 } else {
174 panic!(
175 "Expected Subschema with type: integer, but got: {:?}",
176 &one_of_schema.one_of[1]
177 );
178 }
179
180 let s = r#"
181 false
182 "#;
183 let docs = MarkedYaml::load_from_str(s).unwrap();
184 let value = docs.first().unwrap();
185 let context = crate::Context::with_root_schema(&root_schema, false);
186 let result = root_schema.validate(&context, value);
187
188 assert!(result.is_ok());
189 assert!(!context.has_errors());
190 }
191
192 #[test]
193 fn test_validate_one_of_with_array_of_schemas() {
194 let root_schema = loader::load_from_str(
195 r##"
196 $defs:
197 schema:
198 type: object
199 properties:
200 type:
201 enum: [string, object, number, integer, boolean, enum, array, oneOf, anyOf, not]
202 array_of_schemas:
203 type: array
204 items:
205 $ref: "#/$defs/schema"
206 oneOf:
207 - type: boolean
208 - $ref: "#/$defs/array_of_schemas"
209 "##,
210 )
211 .expect("Failed to load schema");
212 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
213 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
214 };
215 if let Some(_one_of) = &subschema.one_of {
216 } else {
218 panic!("Expected Subschema with oneOf, but got: {subschema:?}");
219 }
220
221 let s = r#"
222 false
223 "#;
224 let docs = MarkedYaml::load_from_str(s).unwrap();
225 let value = docs.first().unwrap();
226 let context = crate::Context::with_root_schema(&root_schema, false);
227 let result = root_schema.validate(&context, value);
228 assert!(result.is_ok());
229 assert!(!context.has_errors());
230 assert!(!context.has_errors());
231 }
232
233 #[test]
234 fn test_validate_one_of_with_null_and_object() {
235 let root_schema = loader::load_from_str(
236 r#"
237 oneOf:
238 - type: null
239 - type: object
240 "#,
241 )
242 .expect("Failed to load schema");
243
244 let s = "null";
245 let docs = MarkedYaml::load_from_str(s).unwrap();
246 let value = docs.first().unwrap();
247 let context = crate::Context::with_root_schema(&root_schema, false);
248 let result = root_schema.validate(&context, value);
249 assert!(result.is_ok());
250 assert!(!context.has_errors());
251
252 let s = r#"
253 name: "John Doe"
254 "#;
255 let docs = MarkedYaml::load_from_str(s).unwrap();
256 let value = docs.first().unwrap();
257 let context = crate::Context::with_root_schema(&root_schema, false);
258 let result = root_schema.validate(&context, value);
259 assert!(result.is_ok());
260 assert!(!context.has_errors());
261 }
262}