valico/json_schema/keywords/
properties.rs

1use 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); // not validated
208                })
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}