valico/json_schema/keywords/
properties.rs1use serde_json::Value;
2use std::collections;
3
4use super::super::helpers;
5use super::super::schema;
6use super::super::validators;
7
8#[allow(missing_copy_implementations)]
9pub struct Properties;
10impl super::Keyword for Properties {
11 fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult {
12 let maybe_properties = def.get("properties");
13 let maybe_additional = def.get("additionalProperties");
14 let maybe_pattern = def.get("patternProperties");
15
16 if maybe_properties.is_none() && maybe_additional.is_none() && maybe_pattern.is_none() {
17 return Ok(None);
18 }
19
20 let properties = if let Some(properties) = maybe_properties {
21 if let Some(properties) = properties.as_object() {
22 let mut schemes = collections::HashMap::new();
23 for (key, value) in properties.iter() {
24 if value.is_object() || value.is_boolean() {
25 schemes.insert(
26 key.to_string(),
27 helpers::alter_fragment_path(
28 ctx.url.clone(),
29 [
30 ctx.escaped_fragment().as_ref(),
31 "properties",
32 helpers::encode(key).as_ref(),
33 ]
34 .join("/"),
35 ),
36 );
37 } else {
38 return Err(schema::SchemaError::Malformed {
39 path: ctx
40 .fragment
41 .iter()
42 .map(String::as_str)
43 .chain(["properties", key])
44 .flat_map(|s| s.chars().chain(['/']))
45 .collect(),
46 detail: "Each value of this object MUST be an object or a boolean"
47 .to_string(),
48 });
49 }
50 }
51 schemes
52 } else {
53 return Err(schema::SchemaError::Malformed {
54 path: ctx.fragment.join("/"),
55 detail: "The value of `properties` MUST be an object.".to_string(),
56 });
57 }
58 } else {
59 collections::HashMap::new()
60 };
61
62 let additional_properties = if let Some(additional_val) = maybe_additional {
63 if additional_val.is_boolean() {
64 validators::properties::AdditionalKind::Boolean(additional_val.as_bool().unwrap())
65 } else if additional_val.is_object() {
66 validators::properties::AdditionalKind::Schema(helpers::alter_fragment_path(
67 ctx.url.clone(),
68 [ctx.escaped_fragment().as_ref(), "additionalProperties"].join("/"),
69 ))
70 } else {
71 return Err(schema::SchemaError::Malformed {
72 path: ctx.fragment.join("/"),
73 detail: "The value of `additionalProperties` MUST be a boolean or an object."
74 .to_string(),
75 });
76 }
77 } else {
78 validators::properties::AdditionalKind::Unspecified
79 };
80
81 let patterns = if let Some(pattern) = maybe_pattern {
82 if pattern.is_object() {
83 let pattern = pattern.as_object().unwrap();
84 let mut patterns = vec![];
85
86 for (key, value) in pattern.iter() {
87 if value.is_object() || value.is_boolean() {
88 match fancy_regex::Regex::new(key.as_ref()) {
89 Ok(regex) => {
90 let url = helpers::alter_fragment_path(ctx.url.clone(), [
91 ctx.escaped_fragment().as_ref(),
92 "patternProperties",
93 helpers::encode(key).as_ref()
94 ].join("/"));
95 patterns.push((regex, url));
96 },
97 Err(_) => {
98 return Err(schema::SchemaError::Malformed {
99 path: ctx.fragment.join("/"),
100 detail: "Each property name of this object SHOULD be a valid regular expression.".to_string()
101 })
102 }
103 }
104 } else {
105 return Err(schema::SchemaError::Malformed {
106 path: ctx.fragment.join("/") + "patternProperties",
107 detail: "Each value of this object MUST be an object or a boolean"
108 .to_string(),
109 });
110 }
111 }
112
113 patterns
114 } else {
115 return Err(schema::SchemaError::Malformed {
116 path: ctx.fragment.join("/"),
117 detail: "The value of `patternProperties` MUST be an object".to_string(),
118 });
119 }
120 } else {
121 vec![]
122 };
123
124 Ok(Some(Box::new(validators::Properties {
125 properties,
126 additional: additional_properties,
127 patterns,
128 })))
129 }
130
131 fn place_first(&self) -> bool {
132 true
133 }
134}
135
136#[cfg(test)]
137use super::super::builder;
138#[cfg(test)]
139use super::super::scope;
140
141#[test]
142fn validate_properties() {
143 let mut scope = scope::Scope::new();
144 let schema = scope
145 .compile_and_return(
146 builder::schema(|s| {
147 s.properties(|props| {
148 props.insert("prop1", |prop1| {
149 prop1.maximum(10f64);
150 });
151 props.insert("prop2", |prop2| {
152 prop2.minimum(11f64);
153 });
154 });
155 })
156 .into_json(),
157 true,
158 )
159 .ok()
160 .unwrap();
161
162 assert_eq!(
163 schema
164 .validate(
165 &jsonway::object(|obj| {
166 obj.set("prop1", 10);
167 obj.set("prop2", 11);
168 })
169 .unwrap()
170 )
171 .is_valid(),
172 true
173 );
174
175 assert_eq!(
176 schema
177 .validate(
178 &jsonway::object(|obj| {
179 obj.set("prop1", 11);
180 obj.set("prop2", 11);
181 })
182 .unwrap()
183 )
184 .is_valid(),
185 false
186 );
187
188 assert_eq!(
189 schema
190 .validate(
191 &jsonway::object(|obj| {
192 obj.set("prop1", 10);
193 obj.set("prop2", 10);
194 })
195 .unwrap()
196 )
197 .is_valid(),
198 false
199 );
200
201 assert_eq!(
202 schema
203 .validate(
204 &jsonway::object(|obj| {
205 obj.set("prop1", 10);
206 obj.set("prop2", 11);
207 obj.set("prop3", 1000); })
209 .unwrap()
210 )
211 .is_valid(),
212 true
213 );
214}
215
216#[test]
217fn validate_kw_properties() {
218 let mut scope = scope::Scope::new();
219 let schema = scope
220 .compile_and_return(
221 builder::schema(|s| {
222 s.properties(|props| {
223 props.insert("id", |prop1| {
224 prop1.maximum(10f64);
225 });
226 props.insert("items", |prop2| {
227 prop2.minimum(11f64);
228 });
229 });
230 })
231 .into_json(),
232 true,
233 )
234 .ok()
235 .unwrap();
236
237 assert_eq!(
238 schema
239 .validate(
240 &jsonway::object(|obj| {
241 obj.set("id", 10);
242 obj.set("items", 11);
243 })
244 .unwrap()
245 )
246 .is_valid(),
247 true
248 );
249
250 assert_eq!(
251 schema
252 .validate(
253 &jsonway::object(|obj| {
254 obj.set("id", 11);
255 obj.set("items", 11);
256 })
257 .unwrap()
258 )
259 .is_valid(),
260 false
261 );
262}
263
264#[test]
265fn validate_pattern_properties() {
266 let mut scope = scope::Scope::new();
267 let schema = scope
268 .compile_and_return(
269 builder::schema(|s| {
270 s.properties(|properties| {
271 properties.insert("prop1", |prop1| {
272 prop1.maximum(10f64);
273 });
274 });
275 s.pattern_properties(|properties| {
276 properties.insert("prop.*", |prop| {
277 prop.maximum(1000f64);
278 });
279 });
280 })
281 .into_json(),
282 true,
283 )
284 .ok()
285 .unwrap();
286
287 assert_eq!(
288 schema
289 .validate(
290 &jsonway::object(|obj| {
291 obj.set("prop1", 11);
292 })
293 .unwrap()
294 )
295 .is_valid(),
296 false
297 );
298
299 assert_eq!(
300 schema
301 .validate(
302 &jsonway::object(|obj| {
303 obj.set("prop1", 10);
304 obj.set("prop2", 1000);
305 })
306 .unwrap()
307 )
308 .is_valid(),
309 true
310 );
311
312 assert_eq!(
313 schema
314 .validate(
315 &jsonway::object(|obj| {
316 obj.set("prop1", 10);
317 obj.set("prop2", 1001);
318 })
319 .unwrap()
320 )
321 .is_valid(),
322 false
323 );
324}
325
326#[test]
327fn validate_additional_properties_false() {
328 let mut scope = scope::Scope::new();
329 let schema = scope
330 .compile_and_return(
331 builder::schema(|s| {
332 s.properties(|properties| {
333 properties.insert("prop1", |prop1| {
334 prop1.maximum(10f64);
335 });
336 });
337 s.pattern_properties(|properties| {
338 properties.insert("prop.*", |prop| {
339 prop.maximum(1000f64);
340 });
341 });
342 s.additional_properties(false);
343 })
344 .into_json(),
345 true,
346 )
347 .ok()
348 .unwrap();
349
350 assert_eq!(
351 schema
352 .validate(
353 &jsonway::object(|obj| {
354 obj.set("prop1", 10);
355 obj.set("prop2", 1000);
356 })
357 .unwrap()
358 )
359 .is_valid(),
360 true
361 );
362
363 assert_eq!(
364 schema
365 .validate(
366 &jsonway::object(|obj| {
367 obj.set("prop1", 10);
368 obj.set("prop2", 1000);
369 obj.set("some_other", 0);
370 })
371 .unwrap()
372 )
373 .is_valid(),
374 false
375 );
376}
377
378#[test]
379fn validate_additional_properties_schema() {
380 let mut scope = scope::Scope::new();
381 let schema = scope
382 .compile_and_return(
383 builder::schema(|s| {
384 s.properties(|properties| {
385 properties.insert("prop1", |prop1| {
386 prop1.maximum(10f64);
387 });
388 });
389 s.pattern_properties(|properties| {
390 properties.insert("prop.*", |prop| {
391 prop.maximum(1000f64);
392 });
393 });
394 s.additional_properties_schema(|additional| additional.maximum(5f64));
395 })
396 .into_json(),
397 true,
398 )
399 .ok()
400 .unwrap();
401
402 assert_eq!(
403 schema
404 .validate(
405 &jsonway::object(|obj| {
406 obj.set("prop1", 10);
407 obj.set("prop2", 1000);
408 obj.set("some_other", 5);
409 })
410 .unwrap()
411 )
412 .is_valid(),
413 true
414 );
415
416 assert_eq!(
417 schema
418 .validate(
419 &jsonway::object(|obj| {
420 obj.set("prop1", 10);
421 obj.set("prop2", 1000);
422 obj.set("some_other", 6);
423 })
424 .unwrap()
425 )
426 .is_valid(),
427 false
428 );
429}
430
431#[test]
432fn malformed() {
433 let mut scope = scope::Scope::new();
434
435 assert!(scope
436 .compile_and_return(
437 jsonway::object(|schema| {
438 schema.set("properties", false);
439 })
440 .unwrap(),
441 true
442 )
443 .is_err());
444
445 assert!(scope
446 .compile_and_return(
447 jsonway::object(|schema| {
448 schema.set("patternProperties", false);
449 })
450 .unwrap(),
451 true
452 )
453 .is_err());
454
455 assert!(scope
456 .compile_and_return(
457 jsonway::object(|schema| {
458 schema.object("patternProperties", |pattern| pattern.set("test", 1));
459 })
460 .unwrap(),
461 true
462 )
463 .is_err());
464
465 assert!(scope
466 .compile_and_return(
467 jsonway::object(|schema| {
468 schema.object("patternProperties", |pattern| {
469 pattern.object("((", |_malformed| {})
470 });
471 })
472 .unwrap(),
473 true
474 )
475 .is_err());
476
477 assert!(scope
478 .compile_and_return(
479 jsonway::object(|schema| {
480 schema.set("additionalProperties", 10);
481 })
482 .unwrap(),
483 true
484 )
485 .is_err());
486}