Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(13)

Side by Side Diff: chrome/common/json_schema_validator.cc

Issue 4673001: Implements a C++ version of JSONSchemaValidator. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: more tests Created 10 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 #include "chrome/common/json_schema_validator.h"
2
3 #include <cmath>
4 #include <limits>
5
6 #include "app/l10n_util.h"
7 #include "base/string_number_conversions.h"
8 #include "base/string_util.h"
9 #include "base/values.h"
10 #include "grit/generated_resources.h"
11
12 namespace {
13
14 double GetNumberValue(Value* value) {
15 double result = 0;
16 if (value->GetAsReal(&result))
17 return result;
18
19 int int_result = 0;
20 if (value->GetAsInteger(&int_result)) {
21 return int_result;
22 }
23
24 NOTREACHED() << "Unexpected value type: " << value->GetType();
25 return 0;
26 }
27
28 bool GetNumberFromDictionary(DictionaryValue* value, const std::string& key,
29 double* number) {
30 if (value->GetReal(key, number))
31 return true;
32
33 int int_value = 0;
34 if (value->GetInteger(key, &int_value)) {
35 *number = int_value;
36 return true;
37 }
38
39 return false;
40 }
41
42 // Classifies a Value as one of the JSON schema primitive types.
43 std::string GetJSONSchemaType(Value* value) {
44 switch (value->GetType()) {
45 case Value::TYPE_NULL:
46 return "null";
47 case Value::TYPE_BOOLEAN:
48 return "boolean";
49 case Value::TYPE_INTEGER:
50 return "integer";
51 case Value::TYPE_REAL: {
52 double double_value = 0;
53 value->GetAsReal(&double_value);
54 return double_value > floor(double_value) ? "number" : "integer";
asargent_no_longer_on_chrome 2010/11/08 23:47:18 Should integral doubles that are > INT_MAX have ty
Aaron Boodman 2010/11/10 19:03:35 I don't think so. "integer" here just means "integ
55 }
56 case Value::TYPE_STRING:
57 return "string";
58 case Value::TYPE_DICTIONARY:
59 return "object";
60 case Value::TYPE_LIST:
61 return "array";
62 default:
63 NOTREACHED() << "Unexpected value type: " << value->GetType();
64 return "";
65 }
66 }
67
68 } // namespace
69
70
71 JSONSchemaValidator::Error::Error() {
72 }
73
74 JSONSchemaValidator::Error::Error(const std::string& message)
75 : path(message) {
76 }
77
78 JSONSchemaValidator::Error::Error(const std::string& path,
79 const std::string& message)
80 : path(path), message(message) {
81 }
82
83
84 const char JSONSchemaValidator::kUnknownTypeReference[] =
85 "Unknown schema reference: *.";
86 const char JSONSchemaValidator::kInvalidChoice[] =
87 "Value does not match any valid type choices.";
88 const char JSONSchemaValidator::kInvalidEnum[] =
89 "Value does not match any valid enum choices.";
90 const char JSONSchemaValidator::kObjectPropertyIsRequired[] =
91 "Property is required.";
92 const char JSONSchemaValidator::kUnexpectedProperty[] =
93 "Unexpected property.";
94 const char JSONSchemaValidator::kArrayMinItems[] =
95 "Array must have at least * items.";
96 const char JSONSchemaValidator::kArrayMaxItems[] =
97 "Array must not have more than * items.";
98 const char JSONSchemaValidator::kArrayItemRequired[] =
99 "Item is required.";
100 const char JSONSchemaValidator::kStringMinLength[] =
101 "String must be at least * characters long.";
102 const char JSONSchemaValidator::kStringMaxLength[] =
103 "String must not be more than * characters long.";
104 const char JSONSchemaValidator::kStringPattern[] =
105 "String must match the pattern: *.";
106 const char JSONSchemaValidator::kInfinityNaNNotSupported[] =
107 "Value must not be infinity or NaN.";
108 const char JSONSchemaValidator::kNumberMinimum[] =
109 "Value must not be less than *.";
110 const char JSONSchemaValidator::kNumberMaximum[] =
111 "Value must not be greater than *.";
112 const char JSONSchemaValidator::kNumberDecimalPlaces[] =
113 "Value must not have more than * decimal places.";
114 const char JSONSchemaValidator::kInvalidType[] =
115 "Expected '*' but got '*'.";
116
117
118 // static
119 std::string JSONSchemaValidator::FormatErrorMessage(const std::string& format,
120 const std::string& s1) {
121 std::string ret_val = format;
122 ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1);
123 return ret_val;
124 }
125
126 // static
127 std::string JSONSchemaValidator::FormatErrorMessage(const std::string& format,
128 const std::string& s1,
129 const std::string& s2) {
130 std::string ret_val = format;
131 ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1);
132 ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s2);
133 return ret_val;
134 }
135
136 JSONSchemaValidator::JSONSchemaValidator(DictionaryValue* schema)
137 : schema_root_(schema), default_allow_additional_properties_(false) {
138 }
139
140 JSONSchemaValidator::JSONSchemaValidator(DictionaryValue* schema,
141 ListValue* types)
142 : schema_root_(schema), default_allow_additional_properties_(false) {
143 if (!types)
144 return;
145
146 for (size_t i = 0; i < types->GetSize(); ++i) {
147 DictionaryValue* type = NULL;
148 CHECK(types->GetDictionary(i, &type));
149
150 std::string id;
151 CHECK(type->GetString("id", &id));
152
153 types_[id] = type;
154 }
155 }
156
157
158 bool JSONSchemaValidator::Validate(Value* instance) {
159 errors_.clear();
160 Validate(instance, schema_root_, "");
161 return errors_.empty();
162 }
163
164 void JSONSchemaValidator::Validate(Value* instance,
165 DictionaryValue* schema,
166 const std::string& path) {
167 // If this schema defines itself as reference type, save it in this.types.
168 std::string id;
169 if (schema->GetString("id", &id))
170 types_[id] = schema;
asargent_no_longer_on_chrome 2010/11/08 23:47:18 What if the schema had 2 different type definition
Aaron Boodman 2010/11/10 19:03:35 Good idea. Done.
171
172 // If the schema has a $ref property, the instance must validate against
173 // that schema. It must be present in types_ to be referenced.
174 std::string ref;
175 if (schema->GetString("$ref", &ref)) {
176 TypeMap::iterator type = types_.find(ref);
177 if (type == types_.end()) {
178 errors_.push_back(
179 Error(path, FormatErrorMessage(kUnknownTypeReference, ref)));
180 } else {
181 Validate(instance, type->second, path);
182 }
183 return;
184 }
185
186 // If the schema has a choices property, the instance must validate against at
187 // least one of the items in that array.
188 ListValue* choices = NULL;
189 if (schema->GetList("choices", &choices)) {
190 ValidateChoices(instance, choices, path);
191 return;
192 }
193
194 // If the schema has an enum property, the instance must be one of those
195 // values.
196 ListValue* enumeration = NULL;
197 if (schema->GetList("enum", &enumeration)) {
198 ValidateEnum(instance, enumeration, path);
199 return;
200 }
201
202 std::string type;
203 schema->GetString("type", &type);
204 CHECK(!type.empty());
205 if (type != "any") {
206 if (!ValidateType(instance, type, path))
207 return;
208
209 // These casts are safe because of checks in ValidateType().
210 if (type == "object")
211 ValidateObject(static_cast<DictionaryValue*>(instance), schema, path);
212 else if (type == "array")
213 ValidateArray(static_cast<ListValue*>(instance), schema, path);
214 else if (type == "string")
215 ValidateString(static_cast<StringValue*>(instance), schema, path);
216 else if (type == "number" || type == "integer")
217 ValidateNumber(instance, schema, path);
218 else if (type != "boolean" && type != "null")
219 CHECK(false) << "Unexpected type: " << type;
220 }
221 }
222
223 void JSONSchemaValidator::ValidateChoices(Value* instance,
224 ListValue* choices,
225 const std::string& path) {
226 size_t original_num_errors = errors_.size();
227
228 for (size_t i = 0; i < choices->GetSize(); ++i) {
229 DictionaryValue* choice = NULL;
230 CHECK(choices->GetDictionary(i, &choice));
231
232 errors_.resize(original_num_errors);
asargent_no_longer_on_chrome 2010/11/08 23:47:18 It took me a moment to grok what you're doing with
Aaron Boodman 2010/11/10 19:03:35 Good ideas. Done.
233 Validate(instance, choice, path);
234 if (errors_.size() == original_num_errors)
235 return;
236 }
237
238 errors_.resize(original_num_errors);
239 errors_.push_back(Error(path, kInvalidChoice));
240 return;
241 }
242
243 void JSONSchemaValidator::ValidateEnum(Value* instance,
244 ListValue* choices,
245 const std::string& path) {
246 for (size_t i = 0; i < choices->GetSize(); ++i) {
247 Value* choice = NULL;
248 choices->Get(i, &choice);
asargent_no_longer_on_chrome 2010/11/08 23:47:18 should you DCHECK that choice is not null after th
Aaron Boodman 2010/11/10 19:03:35 Done.
249 switch (choice->GetType()) {
250 case Value::TYPE_NULL:
251 case Value::TYPE_BOOLEAN:
252 case Value::TYPE_STRING:
253 if (instance->Equals(choice))
254 return;
255
256 case Value::TYPE_INTEGER:
257 case Value::TYPE_REAL:
258 if (instance->IsType(Value::TYPE_INTEGER) ||
259 instance->IsType(Value::TYPE_REAL)) {
260 if (GetNumberValue(choice) == GetNumberValue(instance))
261 return;
262 }
263
264 default:
265 NOTREACHED() << "Unexpected type in enum: " << choice->GetType();
266 }
267 }
268
269 errors_.push_back(Error(path, kInvalidEnum));
270 }
271
272 void JSONSchemaValidator::ValidateObject(DictionaryValue* instance,
273 DictionaryValue* schema,
274 const std::string& path) {
275 DictionaryValue* properties = NULL;
276 CHECK(schema->GetDictionary("properties", &properties));
277 for (DictionaryValue::key_iterator key = properties->begin_keys();
278 key != properties->end_keys(); ++key) {
279 std::string prop_path = path.empty() ? *key : (path + "." + *key);
280 DictionaryValue* prop_schema = NULL;
281 CHECK(properties->GetDictionary(*key, &prop_schema));
282
283 Value* prop_value = NULL;
284 if (instance->Get(*key, &prop_value)) {
285 Validate(prop_value, prop_schema, prop_path);
286 } else {
287 // Properties are required unless there is an optional field set to
288 // 'true'.
289 bool is_optional = false;
290 prop_schema->GetBoolean("optional", &is_optional);
291 if (!is_optional) {
292 errors_.push_back(Error(prop_path, kObjectPropertyIsRequired));
293 }
294 }
295 }
296
297 DictionaryValue* additional_properties_schema = NULL;
298 if (SchemaAllowsAnyAdditionalItems(schema, &additional_properties_schema))
299 return;
300
301 // Validate additional properties.
302 for (DictionaryValue::key_iterator key = instance->begin_keys();
303 key != instance->end_keys(); ++key) {
304 if (properties->HasKey(*key))
305 continue;
306
307 std::string prop_path = path.empty() ? *key : path + "." + *key;
308 if (!additional_properties_schema) {
309 errors_.push_back(Error(prop_path, kUnexpectedProperty));
310 } else {
311 Value* prop_value = NULL;
312 CHECK(instance->Get(*key, &prop_value));
313 Validate(prop_value, additional_properties_schema, prop_path);
314 }
315 }
316 }
317
318 void JSONSchemaValidator::ValidateArray(ListValue* instance,
319 DictionaryValue* schema,
320 const std::string& path) {
321 DictionaryValue* single_type = NULL;
322 size_t instance_size = instance->GetSize();
323 if (schema->GetDictionary("items", &single_type)) {
324 int min_items = 0;
325 if (single_type->GetInteger("minItems", &min_items)) {
326 CHECK(min_items >= 0);
327 if (instance_size < static_cast<size_t>(min_items)) {
328 errors_.push_back(Error(path, FormatErrorMessage(
329 kArrayMinItems, base::IntToString(min_items))));
330 }
331 }
332
333 int max_items = 0;
334 if (single_type->GetInteger("maxItems", &max_items)) {
335 CHECK(max_items >= 0);
336 if (instance_size > static_cast<size_t>(max_items)) {
337 errors_.push_back(Error(path, FormatErrorMessage(
338 kArrayMaxItems, base::IntToString(max_items))));
339 }
340 }
341
342 // If the items property is a single schema, each item in the array must
343 // validate against that schema.
344 for (size_t i = 0; i < instance_size; ++i) {
345 Value* item = NULL;
346 CHECK(instance->Get(i, &item));
347 std::string i_str = base::UintToString(i);
348 std::string item_path = path.empty() ? i_str : (path + "." + i_str);
349 Validate(item, single_type, item_path);
350 }
351
352 return;
353 }
354
355 // Otherwise, the list must be a tuple type, where each item in the list has a
356 // particular schema.
357 ListValue* tuple_type = NULL;
asargent_no_longer_on_chrome 2010/11/08 23:47:18 Consider splitting the rest of the code from here
Aaron Boodman 2010/11/10 19:03:35 good idea. done.
358 CHECK(schema->GetList("items", &tuple_type));
359 size_t tuple_size = tuple_type->GetSize();
360 for (size_t i = 0; i < tuple_size; ++i) {
361 std::string i_str = base::UintToString(i);
362 std::string item_path = path.empty() ? i_str : (path + "." + i_str);
363 DictionaryValue* item_schema = NULL;
364 CHECK(tuple_type->GetDictionary(i, &item_schema));
365 Value* item_value = NULL;
366 instance->Get(i, &item_value);
367 if (item_value && item_value->GetType() != Value::TYPE_NULL) {
368 Validate(item_value, item_schema, item_path);
369 } else {
370 bool is_optional = false;
371 item_schema->GetBoolean("optional", &is_optional);
372 if (!is_optional) {
373 errors_.push_back(Error(item_path, kArrayItemRequired));
374 return;
375 }
376 }
377 }
378
379 DictionaryValue* additional_properties_schema = NULL;
380 if (SchemaAllowsAnyAdditionalItems(schema, &additional_properties_schema))
381 return;
382
383 if (additional_properties_schema) {
384 // Any additional properties must validate against the additionalProperties
385 // schema.
386 for (size_t i = tuple_size; i < instance_size; ++i) {
387 std::string i_str = base::UintToString(i);
388 std::string item_path = path.empty() ? i_str : (path + "." + i_str);
389 Value* item_value = NULL;
390 CHECK(instance->Get(i, &item_value));
391 Validate(item_value, additional_properties_schema, item_path);
392 }
393 } else if (instance_size > tuple_size) {
394 errors_.push_back(Error(path, FormatErrorMessage(
395 kArrayMaxItems, base::UintToString(tuple_size))));
396 }
397 }
398
399 void JSONSchemaValidator::ValidateString(StringValue* instance,
400 DictionaryValue* schema,
401 const std::string& path) {
402 std::string value;
403 CHECK(instance->GetAsString(&value));
404
405 int min_length = 0;
406 if (schema->GetInteger("minLength", &min_length)) {
407 CHECK(min_length >= 0);
408 if (value.size() < static_cast<size_t>(min_length)) {
409 errors_.push_back(Error(path, FormatErrorMessage(
410 kStringMinLength, base::IntToString(min_length))));
411 }
412 }
413
414 int max_length = 0;
415 if (schema->GetInteger("maxLength", &max_length)) {
416 CHECK(max_length >= 0);
417 if (value.size() > static_cast<size_t>(max_length)) {
418 errors_.push_back(Error(path, FormatErrorMessage(
419 kStringMaxLength, base::IntToString(max_length))));
420 }
421 }
422
423 CHECK(!schema->HasKey("pattern")) << "Pattern is not supported.";
424 }
425
426 void JSONSchemaValidator::ValidateNumber(Value* instance,
427 DictionaryValue* schema,
428 const std::string& path) {
429 double value = GetNumberValue(instance);
430
431 if (value == std::numeric_limits<double>::infinity() ||
432 value == std::numeric_limits<double>::quiet_NaN() ||
433 value == std::numeric_limits<double>::signaling_NaN()) {
434 errors_.push_back(Error(path, kInfinityNaNNotSupported));
435 return;
436 }
437
438 double minimum = 0;
439 if (GetNumberFromDictionary(schema, "minimum", &minimum)) {
440 if (value < minimum)
441 errors_.push_back(Error(path, FormatErrorMessage(
442 kNumberMinimum, base::DoubleToString(minimum))));
443 }
444
445 double maximum = 0;
446 if (GetNumberFromDictionary(schema, "maximum", &maximum)) {
447 if (value > maximum)
448 errors_.push_back(Error(path, FormatErrorMessage(
449 kNumberMaximum, base::DoubleToString(maximum))));
450 }
451
452 int max_decimal = 0;
453 if (schema->GetInteger("maxDecimal", &max_decimal)) {
454 double int_part = 0;
455 double fraction_part = modf(value * max_decimal, &int_part);
456 if (fraction_part > 0)
457 errors_.push_back(Error(path, FormatErrorMessage(
458 kNumberDecimalPlaces, base::IntToString(max_decimal))));
459 }
460 }
461
462 bool JSONSchemaValidator::ValidateType(Value* instance,
463 const std::string& expected_type,
464 const std::string& path) {
465 std::string actual_type = GetJSONSchemaType(instance);
466 if (expected_type == actual_type ||
467 (expected_type == "number" && actual_type == "integer")) {
468 return true;
469 } else {
470 errors_.push_back(Error(path, FormatErrorMessage(
471 kInvalidType, expected_type, actual_type)));
472 return false;
473 }
474 }
475
476 bool JSONSchemaValidator::SchemaAllowsAnyAdditionalItems(
477 DictionaryValue* schema, DictionaryValue** additional_properties_schema) {
478 // If the validator allows additional properties globally, and this schema
479 // doesn't override, then we can exit early.
480 schema->GetDictionary("additionalProperties", additional_properties_schema);
481
482 if (*additional_properties_schema) {
483 std::string additional_properties_type("any");
484 CHECK((*additional_properties_schema)->GetString(
485 "type", &additional_properties_type));
486 return additional_properties_type == "any";
487 } else {
488 return default_allow_additional_properties_;
489 }
490 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698