Skip to main content

valico/json_schema/keywords/
items.rs

1use serde_json::Value;
2
3use super::super::helpers;
4use super::super::schema;
5use super::super::validators;
6
7#[allow(missing_copy_implementations)]
8pub struct Items;
9impl super::Keyword for Items {
10    fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult {
11        let maybe_items = def.get("items");
12        let maybe_additional = def.get("additionalItems");
13
14        if !(maybe_items.is_some() || maybe_additional.is_some()) {
15            return Ok(None);
16        }
17
18        let items = if let Some(items_val) = maybe_items {
19            Some(if items_val.is_object() || items_val.is_boolean() {
20                validators::items::ItemsKind::Schema(helpers::alter_fragment_path(
21                    ctx.url.clone(),
22                    [ctx.escaped_fragment().as_ref(), "items"].join("/"),
23                ))
24            } else if items_val.is_array() {
25                let mut schemas = vec![];
26                for (idx, item) in items_val.as_array().unwrap().iter().enumerate() {
27                    if item.is_object() || item.is_boolean() {
28                        schemas.push(helpers::alter_fragment_path(
29                            ctx.url.clone(),
30                            [
31                                ctx.escaped_fragment().as_ref(),
32                                "items",
33                                idx.to_string().as_ref(),
34                            ]
35                            .join("/"),
36                        ))
37                    } else {
38                        return Err(schema::SchemaError::Malformed {
39                            path: ctx.fragment.join("/"),
40                            detail: "Items of this array MUST be objects or booleans".to_string(),
41                        });
42                    }
43                }
44
45                validators::items::ItemsKind::Array(schemas)
46            } else {
47                return Err(schema::SchemaError::Malformed {
48                    path: ctx.fragment.join("/"),
49                    detail: "`items` must be an object, an array or a boolean".to_string(),
50                });
51            })
52        } else {
53            None
54        };
55
56        let additional_items = if let Some(additional_val) = maybe_additional {
57            Some(if additional_val.is_boolean() {
58                validators::items::AdditionalKind::Boolean(additional_val.as_bool().unwrap())
59            } else if additional_val.is_object() {
60                validators::items::AdditionalKind::Schema(helpers::alter_fragment_path(
61                    ctx.url.clone(),
62                    [ctx.escaped_fragment().as_ref(), "additionalItems"].join("/"),
63                ))
64            } else {
65                return Err(schema::SchemaError::Malformed {
66                    path: ctx.fragment.join("/"),
67                    detail: "`additionalItems` must be a boolean or an object".to_string(),
68                });
69            })
70        } else {
71            None
72        };
73
74        Ok(Some(Box::new(validators::Items {
75            items,
76            additional: additional_items,
77        })))
78    }
79
80    fn place_first(&self) -> bool {
81        true
82    }
83}
84
85#[cfg(test)]
86use super::super::builder;
87#[cfg(test)]
88use super::super::scope;
89#[cfg(test)]
90use serde_json::to_value;
91
92#[cfg(test)]
93fn mk_schema() -> Value {
94    json!({
95        "properties": {
96            "a": {
97                "items": {
98                    "properties": {
99                        "x": { "type": "number", "default": 42 }
100                    }
101                }
102            },
103            "b": {
104                "items": [
105                    {
106                        "properties": {
107                            "x": { "type": "boolean", "default": true }
108                        }
109                    },
110                    { "type": "number", "default": 42 }
111                ]
112            }
113        }
114    })
115}
116
117#[test]
118fn default_for_schema() {
119    let mut scope = scope::Scope::new().supply_defaults();
120    let schema = scope.compile_and_return(mk_schema(), true).unwrap();
121    // the "a" branch does not get a default since items is not an array
122    assert_eq!(schema.get_default(), Some(json!({"b": [{"x": true}, 42]})));
123}
124
125#[test]
126fn default_when_needed() {
127    let mut scope = scope::Scope::new().supply_defaults();
128    let schema = scope.compile_and_return(mk_schema(), true).unwrap();
129    let result = schema.validate(&json!({"a": [{}, {"x": 43}], "b": [{"x": false}]}));
130    assert!(result.is_strictly_valid());
131    assert_eq!(
132        result.replacement,
133        Some(json!({"a": [{"x": 42}, {"x": 43}], "b": [{"x": false}, 42]}))
134    );
135}
136
137#[test]
138fn no_default_otherwise() {
139    let mut scope = scope::Scope::new().supply_defaults();
140    let schema = scope.compile_and_return(mk_schema(), true).unwrap();
141    let result = schema.validate(&json!({"a": [], "b": [{"x": false}, 45]}));
142    assert!(result.is_strictly_valid());
143    assert_eq!(result.replacement, None);
144}
145
146#[test]
147fn validate_items_with_schema() {
148    let mut scope = scope::Scope::new();
149    let schema = scope
150        .compile_and_return(
151            builder::schema(|s| {
152                s.items_schema(|items| {
153                    items.minimum(5f64);
154                    items.maximum(10f64);
155                });
156            })
157            .into_json(),
158            true,
159        )
160        .ok()
161        .unwrap();
162
163    assert_eq!(
164        schema
165            .validate(&to_value([5, 6, 7, 8, 9, 10]).unwrap())
166            .is_valid(),
167        true
168    );
169    assert_eq!(
170        schema
171            .validate(&to_value([4, 5, 6, 7, 8, 9, 10]).unwrap())
172            .is_valid(),
173        false
174    );
175    assert_eq!(
176        schema
177            .validate(&to_value([5, 6, 7, 8, 9, 10, 11]).unwrap())
178            .is_valid(),
179        false
180    );
181}
182
183#[test]
184fn validate_items_with_array_of_schemes() {
185    let mut scope = scope::Scope::new();
186    let schema = scope
187        .compile_and_return(
188            builder::schema(|s| {
189                s.items_array(|items| {
190                    items.push(|item| {
191                        item.minimum(1f64);
192                        item.maximum(3f64);
193                    });
194                    items.push(|item| {
195                        item.minimum(3f64);
196                        item.maximum(6f64);
197                    });
198                })
199            })
200            .into_json(),
201            true,
202        )
203        .ok()
204        .unwrap();
205
206    assert_eq!(schema.validate(&to_value([1]).unwrap()).is_valid(), true);
207    assert_eq!(schema.validate(&to_value([1, 3]).unwrap()).is_valid(), true);
208    assert_eq!(
209        schema.validate(&to_value([1, 3, 100]).unwrap()).is_valid(),
210        true
211    );
212    assert_eq!(
213        schema.validate(&to_value([4, 3]).unwrap()).is_valid(),
214        false
215    );
216    assert_eq!(
217        schema.validate(&to_value([1, 7]).unwrap()).is_valid(),
218        false
219    );
220    assert_eq!(
221        schema.validate(&to_value([4, 7]).unwrap()).is_valid(),
222        false
223    );
224}
225
226#[test]
227fn validate_items_with_array_of_schemes_with_additional_bool() {
228    let mut scope = scope::Scope::new();
229    let schema = scope
230        .compile_and_return(
231            builder::schema(|s| {
232                s.items_array(|items| {
233                    items.push(|item| {
234                        item.minimum(1f64);
235                        item.maximum(3f64);
236                    });
237                    items.push(|item| {
238                        item.minimum(3f64);
239                        item.maximum(6f64);
240                    });
241                });
242                s.additional_items(false);
243            })
244            .into_json(),
245            true,
246        )
247        .ok()
248        .unwrap();
249
250    assert_eq!(
251        schema.validate(&to_value([1, 3, 100]).unwrap()).is_valid(),
252        false
253    );
254}
255
256#[test]
257fn validate_items_with_array_of_schemes_with_additional_schema() {
258    let mut scope = scope::Scope::new();
259    let schema = scope
260        .compile_and_return(
261            builder::schema(|s| {
262                s.items_array(|items| {
263                    items.push(|item| {
264                        item.minimum(1f64);
265                        item.maximum(3f64);
266                    });
267                    items.push(|item| {
268                        item.minimum(3f64);
269                        item.maximum(6f64);
270                    });
271                });
272                s.additional_items_schema(|add| add.maximum(100f64));
273            })
274            .into_json(),
275            true,
276        )
277        .ok()
278        .unwrap();
279
280    assert_eq!(
281        schema.validate(&to_value([1, 3, 100]).unwrap()).is_valid(),
282        true
283    );
284    assert_eq!(
285        schema.validate(&to_value([1, 3, 101]).unwrap()).is_valid(),
286        false
287    );
288}