2017
[folly.git] / folly / experimental / test / JSONSchemaTest.cpp
1 /*
2  * Copyright 2017 Facebook, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 // Copyright 2004-present Facebook. All Rights Reserved.
17
18 #include <folly/experimental/JSONSchema.h>
19 #include <folly/json.h>
20 #include <folly/portability/GTest.h>
21
22 using folly::dynamic;
23 using folly::parseJson;
24 using namespace folly::jsonschema;
25 using namespace std;
26
27 bool check(const dynamic& schema, const dynamic& value, bool check = true) {
28   if (check) {
29     auto schemavalidator = makeSchemaValidator();
30     auto ew = schemavalidator->try_validate(schema);
31     if (ew) {
32       return false;
33     }
34   }
35
36   auto validator = makeValidator(schema);
37   auto ew = validator->try_validate(value);
38   if (validator->try_validate(value)) {
39     return false;
40   }
41   return true;
42 }
43
44 TEST(JSONSchemaTest, TestMultipleOfInt) {
45   dynamic schema = dynamic::object("multipleOf", 2);
46   ASSERT_TRUE(check(schema, "invalid"));
47   ASSERT_TRUE(check(schema, 30));
48   ASSERT_TRUE(check(schema, 24.0));
49   ASSERT_FALSE(check(schema, 5));
50   ASSERT_FALSE(check(schema, 2.01));
51 }
52
53 TEST(JSONSchemaTest, TestMultipleOfDouble) {
54   dynamic schema = dynamic::object("multipleOf", 1.5);
55   ASSERT_TRUE(check(schema, "invalid"));
56   ASSERT_TRUE(check(schema, 30));
57   ASSERT_TRUE(check(schema, 24.0));
58   ASSERT_FALSE(check(schema, 5));
59   ASSERT_FALSE(check(schema, 2.01));
60
61   schema = dynamic::object("multipleOf", 0.0001);
62   ASSERT_TRUE(check(schema, 0.0075));
63 }
64
65 TEST(JSONSchemaTest, TestMinimumIntInclusive) {
66   dynamic schema = dynamic::object("minimum", 2);
67   ASSERT_TRUE(check(schema, "invalid"));
68   ASSERT_TRUE(check(schema, 30));
69   ASSERT_TRUE(check(schema, 24.0));
70   ASSERT_TRUE(check(schema, 2));
71   ASSERT_FALSE(check(schema, 1));
72   ASSERT_FALSE(check(schema, 1.9999));
73 }
74
75 TEST(JSONSchemaTest, TestMinimumIntExclusive) {
76   dynamic schema = dynamic::object("minimum", 2)("exclusiveMinimum", true);
77   ASSERT_FALSE(check(schema, 2));
78 }
79
80 TEST(JSONSchemaTest, TestMaximumIntInclusive) {
81   dynamic schema = dynamic::object("maximum", 12);
82   ASSERT_TRUE(check(schema, "invalid"));
83   ASSERT_TRUE(check(schema, 3));
84   ASSERT_TRUE(check(schema, 3.1));
85   ASSERT_TRUE(check(schema, 12));
86   ASSERT_FALSE(check(schema, 13));
87   ASSERT_FALSE(check(schema, 12.0001));
88 }
89
90 TEST(JSONSchemaTest, TestMaximumIntExclusive) {
91   dynamic schema = dynamic::object("maximum", 2)("exclusiveMaximum", true);
92   ASSERT_FALSE(check(schema, 2));
93 }
94
95 TEST(JSONSchemaTest, TestMinimumDoubleInclusive) {
96   dynamic schema = dynamic::object("minimum", 1.75);
97   ASSERT_TRUE(check(schema, "invalid"));
98   ASSERT_TRUE(check(schema, 30));
99   ASSERT_TRUE(check(schema, 24.0));
100   ASSERT_TRUE(check(schema, 1.75));
101   ASSERT_FALSE(check(schema, 1));
102   ASSERT_FALSE(check(schema, 1.74));
103 }
104
105 TEST(JSONSchemaTest, TestMinimumDoubleExclusive) {
106   dynamic schema = dynamic::object("minimum", 1.75)("exclusiveMinimum", true);
107   ASSERT_FALSE(check(schema, 1.75));
108 }
109
110 TEST(JSONSchemaTest, TestMaximumDoubleInclusive) {
111   dynamic schema = dynamic::object("maximum", 12.75);
112   ASSERT_TRUE(check(schema, "invalid"));
113   ASSERT_TRUE(check(schema, 3));
114   ASSERT_TRUE(check(schema, 3.1));
115   ASSERT_TRUE(check(schema, 12.75));
116   ASSERT_FALSE(check(schema, 13));
117   ASSERT_FALSE(check(schema, 12.76));
118 }
119
120 TEST(JSONSchemaTest, TestMaximumDoubleExclusive) {
121   dynamic schema = dynamic::object("maximum", 12.75)("exclusiveMaximum", true);
122   ASSERT_FALSE(check(schema, 12.75));
123 }
124
125 TEST(JSONSchemaTest, TestInvalidSchema) {
126   dynamic schema = dynamic::object("multipleOf", "invalid");
127   // don't check the schema since it's meant to be invalid
128   ASSERT_TRUE(check(schema, 30, false));
129
130   schema = dynamic::object("minimum", "invalid")("maximum", "invalid");
131   ASSERT_TRUE(check(schema, 2, false));
132
133   schema = dynamic::object("minLength", "invalid")("maxLength", "invalid");
134   ASSERT_TRUE(check(schema, 2, false));
135   ASSERT_TRUE(check(schema, "foo", false));
136 }
137
138 TEST(JSONSchemaTest, TestMinimumStringLength) {
139   dynamic schema = dynamic::object("minLength", 3);
140   ASSERT_TRUE(check(schema, "abcde"));
141   ASSERT_TRUE(check(schema, "abc"));
142   ASSERT_FALSE(check(schema, "a"));
143 }
144
145 TEST(JSONSchemaTest, TestMaximumStringLength) {
146   dynamic schema = dynamic::object("maxLength", 3);
147   ASSERT_FALSE(check(schema, "abcde"));
148   ASSERT_TRUE(check(schema, "abc"));
149   ASSERT_TRUE(check(schema, "a"));
150 }
151
152 TEST(JSONSchemaTest, TestStringPattern) {
153   dynamic schema = dynamic::object("pattern", "[1-9]+");
154   ASSERT_TRUE(check(schema, "123"));
155   ASSERT_FALSE(check(schema, "abc"));
156 }
157
158 TEST(JSONSchemaTest, TestMinimumArrayItems) {
159   dynamic schema = dynamic::object("minItems", 3);
160   ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3, 4, 5)));
161   ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3)));
162   ASSERT_FALSE(check(schema, dynamic::array(1)));
163 }
164
165 TEST(JSONSchemaTest, TestMaximumArrayItems) {
166   dynamic schema = dynamic::object("maxItems", 3);
167   ASSERT_FALSE(check(schema, dynamic::array(1, 2, 3, 4, 5)));
168   ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3)));
169   ASSERT_TRUE(check(schema, dynamic::array(1)));
170   ASSERT_TRUE(check(schema, "foobar"));
171 }
172
173 TEST(JSONSchemaTest, TestArrayUniqueItems) {
174   dynamic schema = dynamic::object("uniqueItems", true);
175   ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3)));
176   ASSERT_FALSE(check(schema, dynamic::array(1, 2, 3, 1)));
177   ASSERT_FALSE(check(schema, dynamic::array("cat", "dog", 1, 2, "cat")));
178   ASSERT_TRUE(check(
179       schema,
180       dynamic::array(
181           dynamic::object("foo", "bar"), dynamic::object("foo", "baz"))));
182
183   schema = dynamic::object("uniqueItems", false);
184   ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3, 1)));
185 }
186
187 TEST(JSONSchemaTest, TestArrayItems) {
188   dynamic schema = dynamic::object("items", dynamic::object("minimum", 2));
189   ASSERT_TRUE(check(schema, dynamic::array(2, 3, 4)));
190   ASSERT_FALSE(check(schema, dynamic::array(3, 4, 1)));
191 }
192
193 TEST(JSONSchemaTest, TestArrayAdditionalItems) {
194   dynamic schema = dynamic::object(
195       "items",
196       dynamic::array(
197           dynamic::object("minimum", 2), dynamic::object("minimum", 1)))(
198       "additionalItems", dynamic::object("minimum", 3));
199   ASSERT_TRUE(check(schema, dynamic::array(2, 1, 3, 3, 3, 3, 4)));
200   ASSERT_FALSE(check(schema, dynamic::array(2, 1, 3, 3, 3, 3, 1)));
201 }
202
203 TEST(JSONSchemaTest, TestArrayNoAdditionalItems) {
204   dynamic schema =
205       dynamic::object("items", dynamic::array(dynamic::object("minimum", 2)))(
206           "additionalItems", false);
207   ASSERT_FALSE(check(schema, dynamic::array(3, 3, 3)));
208 }
209
210 TEST(JSONSchemaTest, TestArrayItemsNotPresent) {
211   dynamic schema = dynamic::object("additionalItems", false);
212   ASSERT_TRUE(check(schema, dynamic::array(3, 3, 3)));
213 }
214
215 TEST(JSONSchemaTest, TestRef) {
216   dynamic schema = dynamic::object(
217       "definitions",
218       dynamic::object("positiveInteger",
219                       dynamic::object("minimum", 1)("type", "integer")))(
220       "items", dynamic::object("$ref", "#/definitions/positiveInteger"));
221   ASSERT_TRUE(check(schema, dynamic::array(1, 2, 3, 4)));
222   ASSERT_FALSE(check(schema, dynamic::array(4, -5)));
223 }
224
225 TEST(JSONSchemaTest, TestRecursiveRef) {
226   dynamic schema = dynamic::object(
227       "properties", dynamic::object("more", dynamic::object("$ref", "#")));
228   dynamic d = dynamic::object;
229   ASSERT_TRUE(check(schema, d));
230   d["more"] = dynamic::object;
231   ASSERT_TRUE(check(schema, d));
232   d["more"]["more"] = dynamic::object;
233   ASSERT_TRUE(check(schema, d));
234   d["more"]["more"]["more"] = dynamic::object;
235   ASSERT_TRUE(check(schema, d));
236 }
237
238 TEST(JSONSchemaTest, TestDoubleRecursiveRef) {
239   dynamic schema =
240       dynamic::object("properties",
241                       dynamic::object("more", dynamic::object("$ref", "#"))(
242                           "less", dynamic::object("$ref", "#")));
243   dynamic d = dynamic::object;
244   ASSERT_TRUE(check(schema, d));
245   d["more"] = dynamic::object;
246   d["less"] = dynamic::object;
247   ASSERT_TRUE(check(schema, d));
248   d["more"]["less"] = dynamic::object;
249   d["less"]["mode"] = dynamic::object;
250   ASSERT_TRUE(check(schema, d));
251 }
252
253 TEST(JSONSchemaTest, TestInfinitelyRecursiveRef) {
254   dynamic schema = dynamic::object("not", dynamic::object("$ref", "#"));
255   auto validator = makeValidator(schema);
256   ASSERT_THROW(validator->validate(dynamic::array(1, 2)), std::runtime_error);
257 }
258
259 TEST(JSONSchemaTest, TestRequired) {
260   dynamic schema = dynamic::object("required", dynamic::array("foo", "bar"));
261   ASSERT_FALSE(check(schema, dynamic::object("foo", 123)));
262   ASSERT_FALSE(check(schema, dynamic::object("bar", 123)));
263   ASSERT_TRUE(check(schema, dynamic::object("bar", 123)("foo", 456)));
264 }
265
266 TEST(JSONSchemaTest, TestMinMaxProperties) {
267   dynamic schema = dynamic::object("minProperties", 1)("maxProperties", 3);
268   dynamic d = dynamic::object;
269   ASSERT_FALSE(check(schema, d));
270   d["a"] = 1;
271   ASSERT_TRUE(check(schema, d));
272   d["b"] = 2;
273   ASSERT_TRUE(check(schema, d));
274   d["c"] = 3;
275   ASSERT_TRUE(check(schema, d));
276   d["d"] = 4;
277   ASSERT_FALSE(check(schema, d));
278 }
279
280 TEST(JSONSchemaTest, TestProperties) {
281   dynamic schema = dynamic::object(
282       "properties", dynamic::object("p1", dynamic::object("minimum", 1)))(
283       "patternProperties", dynamic::object("[0-9]+", dynamic::object))(
284       "additionalProperties", dynamic::object("maximum", 5));
285   ASSERT_TRUE(check(schema, dynamic::object("p1", 1)));
286   ASSERT_FALSE(check(schema, dynamic::object("p1", 0)));
287   ASSERT_TRUE(check(schema, dynamic::object("123", "anything")));
288   ASSERT_TRUE(check(schema, dynamic::object("123", 500)));
289   ASSERT_TRUE(check(schema, dynamic::object("other_property", 4)));
290   ASSERT_FALSE(check(schema, dynamic::object("other_property", 6)));
291 }
292 TEST(JSONSchemaTest, TestPropertyAndPattern) {
293   dynamic schema = dynamic::object
294     ("properties", dynamic::object("p1", dynamic::object("minimum", 1)))
295     ("patternProperties", dynamic::object("p.", dynamic::object("maximum", 5)));
296   ASSERT_TRUE(check(schema, dynamic::object("p1", 3)));
297   ASSERT_FALSE(check(schema, dynamic::object("p1", 0)));
298   ASSERT_FALSE(check(schema, dynamic::object("p1", 6)));
299 }
300
301 TEST(JSONSchemaTest, TestPropertyDependency) {
302   dynamic schema = dynamic::object(
303       "dependencies", dynamic::object("p1", dynamic::array("p2")));
304   ASSERT_TRUE(check(schema, dynamic::object));
305   ASSERT_TRUE(check(schema, dynamic::object("p1", 1)("p2", 1)));
306   ASSERT_FALSE(check(schema, dynamic::object("p1", 1)));
307 }
308
309 TEST(JSONSchemaTest, TestSchemaDependency) {
310   dynamic schema = dynamic::object(
311       "dependencies",
312       dynamic::object("p1", dynamic::object("required", dynamic::array("p2"))));
313   ASSERT_TRUE(check(schema, dynamic::object));
314   ASSERT_TRUE(check(schema, dynamic::object("p1", 1)("p2", 1)));
315   ASSERT_FALSE(check(schema, dynamic::object("p1", 1)));
316 }
317
318 TEST(JSONSchemaTest, TestEnum) {
319   dynamic schema = dynamic::object("enum", dynamic::array("a", 1));
320   ASSERT_TRUE(check(schema, "a"));
321   ASSERT_TRUE(check(schema, 1));
322   ASSERT_FALSE(check(schema, "b"));
323 }
324
325 TEST(JSONSchemaTest, TestType) {
326   dynamic schema = dynamic::object("type", "object");
327   ASSERT_TRUE(check(schema, dynamic::object));
328   ASSERT_FALSE(check(schema, dynamic(5)));
329 }
330
331 TEST(JSONSchemaTest, TestTypeArray) {
332   dynamic schema = dynamic::object("type", dynamic::array("array", "number"));
333   ASSERT_TRUE(check(schema, dynamic(5)));
334   ASSERT_TRUE(check(schema, dynamic(1.1)));
335   ASSERT_FALSE(check(schema, dynamic::object));
336 }
337
338 TEST(JSONSchemaTest, TestAllOf) {
339   dynamic schema = dynamic::object(
340       "allOf",
341       dynamic::array(
342           dynamic::object("minimum", 1), dynamic::object("type", "integer")));
343   ASSERT_TRUE(check(schema, 2));
344   ASSERT_FALSE(check(schema, 0));
345   ASSERT_FALSE(check(schema, 1.1));
346 }
347
348 TEST(JSONSchemaTest, TestAnyOf) {
349   dynamic schema = dynamic::object(
350       "anyOf",
351       dynamic::array(
352           dynamic::object("minimum", 1), dynamic::object("type", "integer")));
353   ASSERT_TRUE(check(schema, 2)); // matches both
354   ASSERT_FALSE(check(schema, 0.1)); // matches neither
355   ASSERT_TRUE(check(schema, 1.1)); // matches first one
356   ASSERT_TRUE(check(schema, 0)); // matches second one
357 }
358
359 TEST(JSONSchemaTest, TestOneOf) {
360   dynamic schema = dynamic::object(
361       "oneOf",
362       dynamic::array(
363           dynamic::object("minimum", 1), dynamic::object("type", "integer")));
364   ASSERT_FALSE(check(schema, 2)); // matches both
365   ASSERT_FALSE(check(schema, 0.1)); // matches neither
366   ASSERT_TRUE(check(schema, 1.1)); // matches first one
367   ASSERT_TRUE(check(schema, 0)); // matches second one
368 }
369
370 TEST(JSONSchemaTest, TestNot) {
371   dynamic schema =
372       dynamic::object("not", dynamic::object("minimum", 5)("maximum", 10));
373   ASSERT_TRUE(check(schema, 4));
374   ASSERT_FALSE(check(schema, 7));
375   ASSERT_TRUE(check(schema, 11));
376 }
377
378 // The tests below use some sample schema from json-schema.org
379
380 TEST(JSONSchemaTest, TestMetaSchema) {
381   const char* example1 =
382       "\
383     { \
384       \"title\": \"Example Schema\", \
385       \"type\": \"object\", \
386       \"properties\": { \
387         \"firstName\": { \
388           \"type\": \"string\" \
389         }, \
390         \"lastName\": { \
391           \"type\": \"string\" \
392         }, \
393         \"age\": { \
394           \"description\": \"Age in years\", \
395           \"type\": \"integer\", \
396           \"minimum\": 0 \
397         } \
398       }, \
399       \"required\": [\"firstName\", \"lastName\"] \
400     }";
401
402   auto val = makeSchemaValidator();
403   val->validate(parseJson(example1)); // doesn't throw
404
405   ASSERT_THROW(val->validate("123"), std::runtime_error);
406 }
407
408 TEST(JSONSchemaTest, TestProductSchema) {
409   const char* productSchema =
410       "\
411   { \
412     \"$schema\": \"http://json-schema.org/draft-04/schema#\", \
413       \"title\": \"Product\", \
414       \"description\": \"A product from Acme's catalog\", \
415       \"type\": \"object\", \
416       \"properties\": { \
417         \"id\": { \
418           \"description\": \"The unique identifier for a product\", \
419           \"type\": \"integer\" \
420         }, \
421         \"name\": { \
422           \"description\": \"Name of the product\", \
423           \"type\": \"string\" \
424         }, \
425         \"price\": { \
426           \"type\": \"number\", \
427           \"minimum\": 0, \
428           \"exclusiveMinimum\": true \
429         }, \
430         \"tags\": { \
431           \"type\": \"array\", \
432           \"items\": { \
433             \"type\": \"string\" \
434           }, \
435           \"minItems\": 1, \
436           \"uniqueItems\": true \
437         } \
438       }, \
439       \"required\": [\"id\", \"name\", \"price\"] \
440   }";
441   const char* product =
442       "\
443   { \
444     \"id\": 1, \
445     \"name\": \"A green door\", \
446     \"price\": 12.50, \
447     \"tags\": [\"home\", \"green\"] \
448   }";
449   ASSERT_TRUE(check(parseJson(productSchema), parseJson(product)));
450 }