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