Skip to main content

yaml_schema/schemas/
any_of.rs

1use log::debug;
2use saphyr::AnnotatedMapping;
3use saphyr::MarkedYaml;
4use saphyr::YamlData;
5
6use crate::Context;
7use crate::Error;
8use crate::Result;
9use crate::Validator;
10use crate::YamlSchema;
11use crate::loader;
12use crate::utils::format_vec;
13
14/// The `anyOf` schema is a schema that matches if any of the schemas in the `anyOf` array match.
15/// The schemas are tried in order, and the first match is used. If no match is found, an error is added
16/// to the context.
17#[derive(Debug, Default, PartialEq)]
18pub struct AnyOfSchema {
19    pub any_of: Vec<YamlSchema>,
20}
21
22impl std::fmt::Display for AnyOfSchema {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(f, "anyOf:{}", format_vec(&self.any_of))
25    }
26}
27
28impl<'r> TryFrom<&MarkedYaml<'r>> for AnyOfSchema {
29    type Error = crate::Error;
30
31    fn try_from(value: &MarkedYaml<'r>) -> Result<Self> {
32        if let YamlData::Mapping(mapping) = &value.data {
33            AnyOfSchema::try_from(mapping)
34        } else {
35            Err(expected_mapping!(value))
36        }
37    }
38}
39
40impl<'r> TryFrom<&AnnotatedMapping<'r, MarkedYaml<'r>>> for AnyOfSchema {
41    type Error = crate::Error;
42
43    fn try_from(mapping: &AnnotatedMapping<'r, MarkedYaml<'r>>) -> crate::Result<Self> {
44        let mut any_of_schema = AnyOfSchema::default();
45        if let Some(value) = mapping.get(&MarkedYaml::value_from_str("anyOf")) {
46            any_of_schema.any_of = loader::load_array_of_schemas_marked(value)?;
47        } else {
48            debug!("[anyOf] No `anyOf` key found!");
49        }
50        Ok(any_of_schema)
51    }
52}
53
54impl Validator for crate::schemas::AnyOfSchema {
55    fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
56        let any_of_is_valid = validate_any_of(&self.any_of, context, value)?;
57        debug!("any_of_is_valid: {any_of_is_valid}");
58        if !any_of_is_valid {
59            debug!("AnyOf: None of the schemas in `anyOf` matched!");
60            context.add_error(value, "None of the schemas in `anyOf` matched!");
61            fail_fast!(context);
62        }
63        Ok(())
64    }
65}
66
67pub fn validate_any_of(
68    schemas: &[YamlSchema],
69    context: &Context,
70    marked_yaml: &saphyr::MarkedYaml,
71) -> Result<bool> {
72    debug!("[AnyOf] &context: {context:p}");
73    let mut any_ok = false;
74    for schema in schemas {
75        debug!("[AnyOf] Validating value: {marked_yaml:?} against schema: {schema}");
76        let sub_context = context.get_sub_context_fresh_eval();
77        debug!("[AnyOf]     context: {context:?}");
78        debug!("[AnyOf] sub_context: {sub_context:?}");
79        match schema.validate(&sub_context, marked_yaml) {
80            Ok(()) | Err(Error::FailFast) => {
81                if sub_context.has_errors() {
82                    continue;
83                }
84                debug!("[AnyOf] Schema {schema:?} matched");
85                any_ok = true;
86                if let (Some(p), Some(b)) =
87                    (&context.object_evaluated, &sub_context.object_evaluated)
88                {
89                    p.extend(&b.snapshot());
90                }
91                if let (Some(pcell), Some(bcell)) =
92                    (&context.array_unevaluated, &sub_context.array_unevaluated)
93                {
94                    let snap = bcell.borrow().clone();
95                    pcell.borrow_mut().merge_from(&snap);
96                }
97            }
98            Err(e) => return Err(e),
99        }
100    }
101    debug!("[AnyOf] any_ok: {any_ok}");
102    Ok(any_ok)
103}
104
105#[cfg(test)]
106mod tests {
107    use saphyr::MarkedYaml;
108
109    use crate::Context;
110    use crate::Validator as _;
111    use crate::loader;
112
113    #[test]
114    fn test_any_of_with_description() {
115        let schema_str = r#"
116        description: A string or a number
117        anyOf:
118          - type: string
119          - type: number
120        "#;
121        let any_of_schema = loader::load_from_str(schema_str).expect("Failed to load schema");
122
123        // Test string
124        let value_str = r#""I am a string""#;
125        let value = MarkedYaml::value_from_str(value_str);
126        assert!(value.data.is_string(), "Value should be a string");
127        let context = Context::default();
128        any_of_schema
129            .validate(&context, &value)
130            .expect("Validation failed");
131        assert!(!context.has_errors(), "Should accept string");
132
133        // Test number
134        let value_str = "42";
135        let value = MarkedYaml::value_from_str(value_str);
136        assert!(value.data.is_integer(), "Value should be an integer");
137        let context = Context::default();
138        any_of_schema
139            .validate(&context, &value)
140            .expect("Validation failed");
141        assert!(!context.has_errors(), "Should accept number");
142
143        // Test boolean (should fail)
144        let value_str = "true";
145        let value = MarkedYaml::value_from_str(value_str);
146        assert!(value.data.is_boolean(), "Value should be a boolean");
147        let context = Context::default();
148        any_of_schema
149            .validate(&context, &value)
150            .expect("Validation failed");
151        assert!(context.has_errors(), "Should NOT accept boolean");
152    }
153}