valico/json_schema/keywords/
properties.rs

1use std::collections;
2use regex;
3use serde_json::{Value};
4
5use super::super::schema;
6use super::super::validators;
7use super::super::helpers;
8
9#[allow(missing_copy_implementations)]
10pub struct Properties;
11impl super::Keyword for Properties {
12    fn compile(&self, def: &Value, ctx: &schema::WalkContext) -> super::KeywordResult {
13        let maybe_properties = def.get("properties");
14        let maybe_additional = def.get("additionalProperties");
15        let maybe_pattern = def.get("patternProperties");
16
17        if maybe_properties.is_none() && maybe_additional.is_none() && maybe_pattern.is_none() {
18            return Ok(None)
19        }
20
21        let properties = if maybe_properties.is_some() {
22            let properties = maybe_properties.unwrap();
23            if properties.is_object() {
24                let mut schemes = collections::HashMap::new();
25                let properties = properties.as_object().unwrap();
26                for (key, value) in properties.iter() {
27                    if value.is_object() {
28                        schemes.insert(key.to_string(),
29                            helpers::alter_fragment_path(ctx.url.clone(), [
30                                ctx.escaped_fragment().as_ref(),
31                                "properties",
32                                helpers::encode(key).as_ref()
33                            ].join("/"))
34                        );
35                    } else {
36                        return Err(schema::SchemaError::Malformed {
37                            path: ctx.fragment.join("/"),
38                            detail: "Each value of this object MUST be an object".to_string()
39                        })
40                    }
41                }
42                schemes
43            } else {
44                return Err(schema::SchemaError::Malformed {
45                    path: ctx.fragment.join("/"),
46                    detail: "The value of `properties` MUST be an object.".to_string()
47                })
48            }
49        } else {
50            collections::HashMap::new()
51        };
52
53        let additional_properties = if maybe_additional.is_some() {
54            let additional_val = maybe_additional.unwrap();
55            if additional_val.is_boolean() {
56
57                validators::properties::AdditionalKind::Boolean(additional_val.as_bool().unwrap())
58
59            } else if additional_val.is_object() {
60
61                validators::properties::AdditionalKind::Schema(
62                    helpers::alter_fragment_path(ctx.url.clone(), [
63                        ctx.escaped_fragment().as_ref(),
64                        "additionalProperties"
65                    ].join("/"))
66                )
67
68            } else {
69
70                return Err(schema::SchemaError::Malformed {
71                    path: ctx.fragment.join("/"),
72                    detail: "The value of `additionalProperties` MUST be a boolean or an object.".to_string()
73                })
74
75            }
76        } else {
77            validators::properties::AdditionalKind::Boolean(true)
78        };
79
80        let patterns = if maybe_pattern.is_some() {
81            let pattern = maybe_pattern.unwrap();
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() {
88
89                        match regex::Regex::new(key.as_ref()) {
90                            Ok(regex) => {
91                                let url = helpers::alter_fragment_path(ctx.url.clone(), [
92                                    ctx.escaped_fragment().as_ref(),
93                                    "patternProperties",
94                                    helpers::encode(key).as_ref()
95                                ].join("/"));
96                                patterns.push((regex, url));
97                            },
98                            Err(_) => {
99                                return Err(schema::SchemaError::Malformed {
100                                    path: ctx.fragment.join("/"),
101                                    detail: "Each property name of this object SHOULD be a valid regular expression.".to_string()
102                                })
103                            }
104                        }
105
106                    } else {
107                        return Err(schema::SchemaError::Malformed {
108                            path: ctx.fragment.join("/"),
109                            detail: "Each value of this object MUST be an object".to_string()
110                        })
111                    }
112                }
113
114                patterns
115
116            } else {
117                return Err(schema::SchemaError::Malformed {
118                    path: ctx.fragment.join("/"),
119                    detail: "The value of `patternProperties` MUST be an object".to_string()
120                })
121            }
122        } else { vec![] };
123
124        Ok(Some(Box::new(validators::Properties {
125            properties: properties,
126            additional: additional_properties,
127            patterns: patterns
128        })))
129
130    }
131}
132
133#[cfg(test)] use super::super::scope;
134#[cfg(test)] use super::super::builder;
135#[cfg(test)] use jsonway;
136
137#[test]
138fn validate_properties() {
139    let mut scope = scope::Scope::new();
140    let schema = scope.compile_and_return(builder::schema(|s| {
141        s.properties(|props| {
142            props.insert("prop1", |prop1| {
143                prop1.maximum(10f64, false);
144            });
145            props.insert("prop2", |prop2| {
146                prop2.minimum(11f64, false);
147            });
148        });
149    }).into_json(), true).ok().unwrap();
150
151    assert_eq!(schema.validate(&jsonway::object(|obj| {
152        obj.set("prop1", 10);
153        obj.set("prop2", 11);
154    }).unwrap()).is_valid(), true);
155
156    assert_eq!(schema.validate(&jsonway::object(|obj| {
157        obj.set("prop1", 11);
158        obj.set("prop2", 11);
159    }).unwrap()).is_valid(), false);
160
161    assert_eq!(schema.validate(&jsonway::object(|obj| {
162        obj.set("prop1", 10);
163        obj.set("prop2", 10);
164    }).unwrap()).is_valid(), false);
165
166    assert_eq!(schema.validate(&jsonway::object(|obj| {
167        obj.set("prop1", 10);
168        obj.set("prop2", 11);
169        obj.set("prop3", 1000); // not validated
170    }).unwrap()).is_valid(), true);
171}
172
173#[test]
174fn validate_kw_properties() {
175    let mut scope = scope::Scope::new();
176    let schema = scope.compile_and_return(builder::schema(|s| {
177        s.properties(|props| {
178            props.insert("id", |prop1| {
179                prop1.maximum(10f64, false);
180            });
181            props.insert("items", |prop2| {
182                prop2.minimum(11f64, false);
183            });
184        });
185    }).into_json(), true).ok().unwrap();
186
187    assert_eq!(schema.validate(&jsonway::object(|obj| {
188        obj.set("id", 10);
189        obj.set("items", 11);
190    }).unwrap()).is_valid(), true);
191
192    assert_eq!(schema.validate(&jsonway::object(|obj| {
193        obj.set("id", 11);
194        obj.set("items", 11);
195    }).unwrap()).is_valid(), false);
196
197}
198
199#[test]
200fn validate_pattern_properties() {
201    let mut scope = scope::Scope::new();
202    let schema = scope.compile_and_return(builder::schema(|s| {
203        s.properties(|properties| {
204            properties.insert("prop1", |prop1| {
205                prop1.maximum(10f64, false);
206            });
207        });
208        s.pattern_properties(|properties| {
209            properties.insert("prop.*", |prop| {
210                prop.maximum(1000f64, false);
211            });
212        });
213    }).into_json(), true).ok().unwrap();
214
215    assert_eq!(schema.validate(&jsonway::object(|obj| {
216        obj.set("prop1", 11);
217    }).unwrap()).is_valid(), false);
218
219    assert_eq!(schema.validate(&jsonway::object(|obj| {
220        obj.set("prop1", 10);
221        obj.set("prop2", 1000);
222    }).unwrap()).is_valid(), true);
223
224    assert_eq!(schema.validate(&jsonway::object(|obj| {
225        obj.set("prop1", 10);
226        obj.set("prop2", 1001);
227    }).unwrap()).is_valid(), false);
228}
229
230#[test]
231fn validate_additional_properties_false() {
232    let mut scope = scope::Scope::new();
233    let schema = scope.compile_and_return(builder::schema(|s| {
234        s.properties(|properties| {
235            properties.insert("prop1", |prop1| {
236                prop1.maximum(10f64, false);
237            });
238        });
239        s.pattern_properties(|properties| {
240            properties.insert("prop.*", |prop| {
241                prop.maximum(1000f64, false);
242            });
243        });
244        s.additional_properties(false);
245    }).into_json(), true).ok().unwrap();
246
247    assert_eq!(schema.validate(&jsonway::object(|obj| {
248        obj.set("prop1", 10);
249        obj.set("prop2", 1000);
250    }).unwrap()).is_valid(), true);
251
252    assert_eq!(schema.validate(&jsonway::object(|obj| {
253        obj.set("prop1", 10);
254        obj.set("prop2", 1000);
255        obj.set("some_other", 0);
256    }).unwrap()).is_valid(), false);
257}
258
259#[test]
260fn validate_additional_properties_schema() {
261    let mut scope = scope::Scope::new();
262    let schema = scope.compile_and_return(builder::schema(|s| {
263        s.properties(|properties| {
264            properties.insert("prop1", |prop1| {
265                prop1.maximum(10f64, false);
266            });
267        });
268        s.pattern_properties(|properties| {
269            properties.insert("prop.*", |prop| {
270                prop.maximum(1000f64, false);
271            });
272        });
273        s.additional_properties_schema(|additional| {
274            additional.maximum(5f64, false)
275        });
276    }).into_json(), true).ok().unwrap();
277
278    assert_eq!(schema.validate(&jsonway::object(|obj| {
279        obj.set("prop1", 10);
280        obj.set("prop2", 1000);
281        obj.set("some_other", 5);
282    }).unwrap()).is_valid(), true);
283
284    assert_eq!(schema.validate(&jsonway::object(|obj| {
285        obj.set("prop1", 10);
286        obj.set("prop2", 1000);
287        obj.set("some_other", 6);
288    }).unwrap()).is_valid(), false);
289}
290
291#[test]
292fn malformed() {
293    let mut scope = scope::Scope::new();
294
295    assert!(scope.compile_and_return(jsonway::object(|schema| {
296        schema.set("properties", false);
297    }).unwrap(), true).is_err());
298
299    assert!(scope.compile_and_return(jsonway::object(|schema| {
300        schema.set("patternProperties", false);
301    }).unwrap(), true).is_err());
302
303    assert!(scope.compile_and_return(jsonway::object(|schema| {
304        schema.object("patternProperties", |pattern| {
305            pattern.set("test", 1)
306        });
307    }).unwrap(), true).is_err());
308
309    assert!(scope.compile_and_return(jsonway::object(|schema| {
310        schema.object("patternProperties", |pattern| {
311            pattern.object("((", |_malformed| {})
312        });
313    }).unwrap(), true).is_err());
314
315    assert!(scope.compile_and_return(jsonway::object(|schema| {
316        schema.set("additionalProperties", 10);
317    }).unwrap(), true).is_err());
318}