Skip to main content

yaml_schema/schemas/
array.rs

1use std::fmt::Display;
2
3use log::debug;
4use saphyr::AnnotatedMapping;
5use saphyr::MarkedYaml;
6use saphyr::Scalar;
7use saphyr::YamlData;
8
9use crate::Context;
10use crate::Result;
11use crate::Validator;
12use crate::YamlSchema;
13use crate::loader;
14use crate::schemas::BooleanOrSchema;
15use crate::utils::format_marker;
16use crate::utils::format_vec;
17use crate::utils::format_yaml_data;
18
19/// An array schema represents an array
20#[derive(Debug, Default, PartialEq)]
21pub struct ArraySchema<'r> {
22    pub items: Option<BooleanOrSchema<'r>>,
23    pub prefix_items: Option<Vec<YamlSchema<'r>>>,
24    pub contains: Option<YamlSchema<'r>>,
25}
26
27impl<'r> TryFrom<&AnnotatedMapping<'r, MarkedYaml<'r>>> for ArraySchema<'r> {
28    type Error = crate::Error;
29
30    fn try_from(mapping: &AnnotatedMapping<'r, MarkedYaml<'r>>) -> crate::Result<Self> {
31        let mut array_schema = ArraySchema::default();
32        for (key, value) in mapping.iter() {
33            if let YamlData::Value(Scalar::String(s)) = &key.data {
34                match s.as_ref() {
35                    "contains" => {
36                        if value.data.is_mapping() {
37                            let yaml_schema = value.try_into()?;
38                            array_schema.contains = Some(yaml_schema);
39                        } else {
40                            return Err(generic_error!(
41                                "contains: expected a mapping, but got: {:?}",
42                                value
43                            ));
44                        }
45                    }
46                    "items" => {
47                        let array_items = loader::load_array_items_marked(value)?;
48                        array_schema.items = Some(array_items);
49                    }
50                    "type" => {
51                        if let YamlData::Value(Scalar::String(s)) = &value.data {
52                            if s != "array" {
53                                return Err(unsupported_type!(
54                                    "Expected type: array, but got: {}",
55                                    s
56                                ));
57                            }
58                        } else {
59                            return Err(expected_type_is_string!(value));
60                        }
61                    }
62                    "prefixItems" => {
63                        let prefix_items = loader::load_array_of_schemas_marked(value)?;
64                        array_schema.prefix_items = Some(prefix_items);
65                    }
66                    _ => debug!("Unsupported key for ArraySchema: {}", s),
67                }
68            } else {
69                return Err(generic_error!(
70                    "{} Expected scalar key, got: {:?}",
71                    format_marker(&key.span.start),
72                    key
73                ));
74            }
75        }
76        Ok(array_schema)
77    }
78}
79
80impl Validator for ArraySchema<'_> {
81    fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
82        debug!("[ArraySchema] self: {self:?}");
83        let data = &value.data;
84        debug!("[ArraySchema] Validating value: {}", format_yaml_data(data));
85
86        if let saphyr::YamlData::Sequence(array) = data {
87            // validate contains
88            if let Some(sub_schema) = &self.contains {
89                let any_matches = array.iter().any(|item| {
90                    let sub_context = crate::Context {
91                        root_schema: context.root_schema,
92                        fail_fast: true,
93                        ..Default::default()
94                    };
95                    sub_schema.validate(&sub_context, item).is_ok() && !sub_context.has_errors()
96                });
97                if !any_matches {
98                    context.add_error(value, "Contains validation failed!".to_string());
99                }
100            }
101
102            // validate prefix items
103            if let Some(prefix_items) = &self.prefix_items {
104                debug!(
105                    "[ArraySchema] Validating prefix items: {}",
106                    format_vec(prefix_items)
107                );
108                for (i, item) in array.iter().enumerate() {
109                    // if the index is within the prefix items, validate against the prefix items schema
110                    if i < prefix_items.len() {
111                        debug!(
112                            "[ArraySchema] Validating prefix item {} with schema: {}",
113                            i, prefix_items[i]
114                        );
115                        prefix_items[i].validate(context, item)?;
116                    } else if let Some(items) = &self.items {
117                        // if the index is not within the prefix items, validate against the array items schema
118                        debug!("[ArraySchema] Validating array item {i} with schema: {items}");
119                        match items {
120                            BooleanOrSchema::Boolean(true) => {
121                                // `items: true` allows any items
122                                break;
123                            }
124                            BooleanOrSchema::Boolean(false) => {
125                                context.add_error(
126                                    item,
127                                    "Additional array items are not allowed!".to_string(),
128                                );
129                            }
130                            BooleanOrSchema::Schema(yaml_schema) => {
131                                yaml_schema.validate(context, item)?;
132                            }
133                        }
134                    } else {
135                        break;
136                    }
137                }
138            } else {
139                // validate array items
140                if let Some(items) = &self.items {
141                    match items {
142                        BooleanOrSchema::Boolean(true) => { /* no-op */ }
143                        BooleanOrSchema::Boolean(false) => {
144                            if self.prefix_items.is_none() && !array.is_empty() {
145                                context
146                                    .add_error(value, "Array items are not allowed!".to_string());
147                            }
148                        }
149                        BooleanOrSchema::Schema(yaml_schema) => {
150                            for item in array {
151                                yaml_schema.validate(context, item)?;
152                            }
153                        }
154                    }
155                }
156            }
157
158            Ok(())
159        } else {
160            debug!("[ArraySchema] context.fail_fast: {}", context.fail_fast);
161            context.add_error(
162                value,
163                format!(
164                    "Expected an array, but got: {}",
165                    format_yaml_data(&value.data)
166                ),
167            );
168            fail_fast!(context);
169            Ok(())
170        }
171    }
172}
173
174impl Display for ArraySchema<'_> {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        write!(
177            f,
178            "Array{{ items: {:?}, prefix_items: {:?}, contains: {:?}}}",
179            self.items, self.prefix_items, self.contains
180        )
181    }
182}
183#[cfg(test)]
184mod tests {
185    use crate::schemas::NumberSchema;
186    use crate::schemas::StringSchema;
187    use saphyr::LoadableYamlNode;
188
189    use super::*;
190
191    #[test]
192    fn test_array_schema_prefix_items() {
193        let schema = ArraySchema {
194            prefix_items: Some(vec![YamlSchema::typed_number(NumberSchema::default())]),
195            items: Some(BooleanOrSchema::schema(YamlSchema::typed_string(
196                StringSchema::default(),
197            ))),
198            ..Default::default()
199        };
200        let s = r#"
201        - 1
202        - 2
203        - Washington
204        "#;
205        let docs = saphyr::MarkedYaml::load_from_str(s).unwrap();
206        let value = docs.first().unwrap();
207        let context = crate::Context::default();
208        let result = schema.validate(&context, value);
209        assert!(result.is_ok());
210    }
211
212    #[test]
213    fn test_array_schema_prefix_items_from_yaml() {
214        let schema_string = "
215      type: array
216      prefixItems:
217        - type: number
218        - type: string
219        - enum:
220          - Street
221          - Avenue
222          - Boulevard
223        - enum:
224          - NW
225          - NE
226          - SW
227          - SE
228      items:
229        type: string
230";
231
232        let yaml_string = r#"
233        - 1600
234        - Pennsylvania
235        - Avenue
236        - NW
237        - Washington
238        "#;
239
240        let s_docs = saphyr::MarkedYaml::load_from_str(schema_string).unwrap();
241        let first_schema = s_docs.first().unwrap();
242        if let YamlData::Mapping(mapping) = &first_schema.data {
243            let schema = ArraySchema::try_from(mapping).unwrap();
244            let docs = saphyr::MarkedYaml::load_from_str(yaml_string).unwrap();
245            let value = docs.first().unwrap();
246            let context = crate::Context::default();
247            let result = schema.validate(&context, value);
248            assert!(result.is_ok());
249        } else {
250            panic!("Expected first_schema to be a Mapping, but got {first_schema:?}");
251        }
252    }
253
254    #[test]
255    fn array_schema_prefix_items_with_additional_items() {
256        let schema_string = "
257      type: array
258      prefixItems:
259        - type: number
260        - type: string
261        - enum:
262          - Street
263          - Avenue
264          - Boulevard
265        - enum:
266          - NW
267          - NE
268          - SW
269          - SE
270      items:
271        type: string
272";
273
274        let yaml_string = r#"
275        - 1600
276        - Pennsylvania
277        - Avenue
278        - NW
279        - 20500
280        "#;
281
282        let docs = MarkedYaml::load_from_str(schema_string).unwrap();
283        let first_doc = docs.first().unwrap();
284        if let YamlData::Mapping(mapping) = &first_doc.data {
285            let schema: ArraySchema = ArraySchema::try_from(mapping).unwrap();
286            let docs = saphyr::MarkedYaml::load_from_str(yaml_string).unwrap();
287            let value = docs.first().unwrap();
288            let context = crate::Context::default();
289            let result = schema.validate(&context, value);
290            assert!(result.is_ok());
291        } else {
292            panic!("Expected first_doc to be a Mapping, but got {first_doc:?}");
293        }
294    }
295
296    #[test]
297    fn test_contains() {
298        let number_schema = YamlSchema::typed_number(NumberSchema::default());
299        let schema = ArraySchema {
300            contains: Some(number_schema),
301            ..Default::default()
302        };
303        let s = r#"
304        - life
305        - universe
306        - everything
307        - 42
308        "#;
309        let docs = saphyr::MarkedYaml::load_from_str(s).unwrap();
310        let value = docs.first().unwrap();
311        let context = crate::Context::default();
312        let result = schema.validate(&context, value);
313        assert!(result.is_ok());
314        let errors = context.errors.take();
315        assert!(errors.is_empty());
316    }
317
318    #[test]
319    fn test_array_schema_contains_fails() {
320        let number_schema = YamlSchema::typed_number(NumberSchema::default());
321        let schema = ArraySchema {
322            contains: Some(number_schema),
323            ..Default::default()
324        };
325        let s = r#"
326        - life
327        - universe
328        - everything
329        "#;
330        let docs = saphyr::MarkedYaml::load_from_str(s).unwrap();
331        let value = docs.first().unwrap();
332        let context = crate::Context::default();
333        let result = schema.validate(&context, value);
334        assert!(result.is_ok());
335        let errors = context.errors.take();
336        assert!(!errors.is_empty());
337    }
338}