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