yaml_schema/schemas/
array.rs1use 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#[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 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 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 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 debug!("[ArraySchema] Validating array item {i} with schema: {items}");
119 match items {
120 BooleanOrSchema::Boolean(true) => {
121 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 if let Some(items) = &self.items {
141 match items {
142 BooleanOrSchema::Boolean(true) => { }
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}