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