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<'r> {
19    pub any_of: Vec<YamlSchema<'r>>,
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<'r> {
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<'r> {
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    for schema in schemas {
74        debug!("[AnyOf] Validating value: {marked_yaml:?} against schema: {schema}");
75        // Since we're only looking for the first match, we can stop as soon as we find one
76        // That also means that when evaluating sub schemas, we can fail fast to short circuit
77        // the rest of the validation
78        let sub_context = context.get_sub_context();
79        debug!("[AnyOf]     context: {context:?}");
80        debug!("[AnyOf] sub_context: {sub_context:?}");
81        match schema.validate(&sub_context, marked_yaml) {
82            Ok(()) | Err(Error::FailFast) => {
83                if sub_context.has_errors() {
84                    continue;
85                }
86                debug!("[AnyOf] Schema {schema:?} matched");
87                return Ok(true);
88            }
89            Err(e) => return Err(e),
90        }
91    }
92    debug!("[AnyOf] None of the schemas matched");
93    // If we get here, then none of the schemas matched
94    Ok(false)
95}
96
97#[cfg(test)]
98mod tests {
99    use saphyr::MarkedYaml;
100
101    use crate::Context;
102    use crate::Validator as _;
103    use crate::loader;
104
105    #[test]
106    fn test_any_of_with_description() {
107        let schema_str = r#"
108        description: A string or a number
109        anyOf:
110          - type: string
111          - type: number
112        "#;
113        let any_of_schema = loader::load_from_str(schema_str).expect("Failed to load schema");
114
115        // Test string
116        let value_str = r#""I am a string""#;
117        let value = MarkedYaml::value_from_str(value_str);
118        assert!(value.data.is_string(), "Value should be a string");
119        let context = Context::default();
120        any_of_schema
121            .validate(&context, &value)
122            .expect("Validation failed");
123        assert!(!context.has_errors(), "Should accept string");
124
125        // Test number
126        let value_str = "42";
127        let value = MarkedYaml::value_from_str(value_str);
128        assert!(value.data.is_integer(), "Value should be an integer");
129        let context = Context::default();
130        any_of_schema
131            .validate(&context, &value)
132            .expect("Validation failed");
133        assert!(!context.has_errors(), "Should accept number");
134
135        // Test boolean (should fail)
136        let value_str = "true";
137        let value = MarkedYaml::value_from_str(value_str);
138        assert!(value.data.is_boolean(), "Value should be a boolean");
139        let context = Context::default();
140        any_of_schema
141            .validate(&context, &value)
142            .expect("Validation failed");
143        assert!(context.has_errors(), "Should NOT accept boolean");
144    }
145}