Skip to main content

yaml_schema/schemas/
one_of.rs

1use 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/// The `oneOf` schema is a schema that matches if one, and only one of the schemas in the `oneOf` array match.
18/// The schemas are tried in order, and the first match is used. If no match is found, an error is added
19/// to the context.
20#[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            // oneOf schema loaded successfully
217        } 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}