OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 // ----------------------------------------------------------------------------- | |
6 // NOTE: If you change this file you need to touch renderer_resources.grd to | |
7 // have your change take effect. | |
8 // ----------------------------------------------------------------------------- | |
9 | |
10 //============================================================================== | |
11 // This file contains a class that implements a subset of JSON Schema. | |
12 // See: http://www.json.com/json-schema-proposal/ for more details. | |
13 // | |
14 // The following features of JSON Schema are not implemented: | |
15 // - requires | |
16 // - unique | |
17 // - disallow | |
18 // - union types (but replaced with 'choices') | |
19 // | |
20 // The following properties are not applicable to the interface exposed by | |
21 // this class: | |
22 // - options | |
23 // - readonly | |
24 // - title | |
25 // - description | |
26 // - format | |
27 // - default | |
28 // - transient | |
29 // - hidden | |
30 // | |
31 // There are also these departures from the JSON Schema proposal: | |
32 // - function and undefined types are supported | |
33 // - null counts as 'unspecified' for optional values | |
34 // - added the 'choices' property, to allow specifying a list of possible types | |
35 // for a value | |
36 // - by default an "object" typed schema does not allow additional properties. | |
37 // if present, "additionalProperties" is to be a schema against which all | |
38 // additional properties will be validated. | |
39 //============================================================================== | |
40 | |
41 var loadTypeSchema = require('utils').loadTypeSchema; | |
42 var CHECK = requireNative('logging').CHECK; | |
43 | |
44 function isInstanceOfClass(instance, className) { | |
45 while ((instance = instance.__proto__)) { | |
46 if (instance.constructor.name == className) | |
47 return true; | |
48 } | |
49 return false; | |
50 } | |
51 | |
52 function isOptionalValue(value) { | |
53 return typeof(value) === 'undefined' || value === null; | |
54 } | |
55 | |
56 function enumToString(enumValue) { | |
57 if (enumValue.name === undefined) | |
58 return enumValue; | |
59 | |
60 return enumValue.name; | |
61 } | |
62 | |
63 /** | |
64 * Validates an instance against a schema and accumulates errors. Usage: | |
65 * | |
66 * var validator = new JSONSchemaValidator(); | |
67 * validator.validate(inst, schema); | |
68 * if (validator.errors.length == 0) | |
69 * console.log("Valid!"); | |
70 * else | |
71 * console.log(validator.errors); | |
72 * | |
73 * The errors property contains a list of objects. Each object has two | |
74 * properties: "path" and "message". The "path" property contains the path to | |
75 * the key that had the problem, and the "message" property contains a sentence | |
76 * describing the error. | |
77 */ | |
78 function JSONSchemaValidator() { | |
79 this.errors = []; | |
80 this.types = []; | |
81 } | |
82 | |
83 JSONSchemaValidator.messages = { | |
84 invalidEnum: "Value must be one of: [*].", | |
85 propertyRequired: "Property is required.", | |
86 unexpectedProperty: "Unexpected property.", | |
87 arrayMinItems: "Array must have at least * items.", | |
88 arrayMaxItems: "Array must not have more than * items.", | |
89 itemRequired: "Item is required.", | |
90 stringMinLength: "String must be at least * characters long.", | |
91 stringMaxLength: "String must not be more than * characters long.", | |
92 stringPattern: "String must match the pattern: *.", | |
93 numberFiniteNotNan: "Value must not be *.", | |
94 numberMinValue: "Value must not be less than *.", | |
95 numberMaxValue: "Value must not be greater than *.", | |
96 numberIntValue: "Value must fit in a 32-bit signed integer.", | |
97 numberMaxDecimal: "Value must not have more than * decimal places.", | |
98 invalidType: "Expected '*' but got '*'.", | |
99 invalidTypeIntegerNumber: | |
100 "Expected 'integer' but got 'number', consider using Math.round().", | |
101 invalidChoice: "Value does not match any valid type choices.", | |
102 invalidPropertyType: "Missing property type.", | |
103 schemaRequired: "Schema value required.", | |
104 unknownSchemaReference: "Unknown schema reference: *.", | |
105 notInstance: "Object must be an instance of *." | |
106 }; | |
107 | |
108 /** | |
109 * Builds an error message. Key is the property in the |errors| object, and | |
110 * |opt_replacements| is an array of values to replace "*" characters with. | |
111 */ | |
112 JSONSchemaValidator.formatError = function(key, opt_replacements) { | |
113 var message = this.messages[key]; | |
114 if (opt_replacements) { | |
115 for (var i = 0; i < opt_replacements.length; i++) { | |
116 message = message.replace("*", opt_replacements[i]); | |
117 } | |
118 } | |
119 return message; | |
120 }; | |
121 | |
122 /** | |
123 * Classifies a value as one of the JSON schema primitive types. Note that we | |
124 * don't explicitly disallow 'function', because we want to allow functions in | |
125 * the input values. | |
126 */ | |
127 JSONSchemaValidator.getType = function(value) { | |
128 var s = typeof value; | |
129 | |
130 if (s == "object") { | |
131 if (value === null) { | |
132 return "null"; | |
133 } else if (Object.prototype.toString.call(value) == "[object Array]") { | |
134 return "array"; | |
135 } else if (typeof(ArrayBuffer) != "undefined" && | |
136 value.constructor == ArrayBuffer) { | |
137 return "binary"; | |
138 } | |
139 } else if (s == "number") { | |
140 if (value % 1 == 0) { | |
141 return "integer"; | |
142 } | |
143 } | |
144 | |
145 return s; | |
146 }; | |
147 | |
148 /** | |
149 * Add types that may be referenced by validated schemas that reference them | |
150 * with "$ref": <typeId>. Each type must be a valid schema and define an | |
151 * "id" property. | |
152 */ | |
153 JSONSchemaValidator.prototype.addTypes = function(typeOrTypeList) { | |
154 function addType(validator, type) { | |
155 if (!type.id) | |
156 throw new Error("Attempt to addType with missing 'id' property"); | |
157 validator.types[type.id] = type; | |
158 } | |
159 | |
160 if (typeOrTypeList instanceof Array) { | |
161 for (var i = 0; i < typeOrTypeList.length; i++) { | |
162 addType(this, typeOrTypeList[i]); | |
163 } | |
164 } else { | |
165 addType(this, typeOrTypeList); | |
166 } | |
167 } | |
168 | |
169 /** | |
170 * Returns a list of strings of the types that this schema accepts. | |
171 */ | |
172 JSONSchemaValidator.prototype.getAllTypesForSchema = function(schema) { | |
173 var schemaTypes = []; | |
174 if (schema.type) | |
175 $Array.push(schemaTypes, schema.type); | |
176 if (schema.choices) { | |
177 for (var i = 0; i < schema.choices.length; i++) { | |
178 var choiceTypes = this.getAllTypesForSchema(schema.choices[i]); | |
179 schemaTypes = $Array.concat(schemaTypes, choiceTypes); | |
180 } | |
181 } | |
182 var ref = schema['$ref']; | |
183 if (ref) { | |
184 var type = this.getOrAddType(ref); | |
185 CHECK(type, 'Could not find type ' + ref); | |
186 schemaTypes = $Array.concat(schemaTypes, this.getAllTypesForSchema(type)); | |
187 } | |
188 return schemaTypes; | |
189 }; | |
190 | |
191 JSONSchemaValidator.prototype.getOrAddType = function(typeName) { | |
192 if (!this.types[typeName]) | |
193 this.types[typeName] = loadTypeSchema(typeName); | |
194 return this.types[typeName]; | |
195 }; | |
196 | |
197 /** | |
198 * Returns true if |schema| would accept an argument of type |type|. | |
199 */ | |
200 JSONSchemaValidator.prototype.isValidSchemaType = function(type, schema) { | |
201 if (type == 'any') | |
202 return true; | |
203 | |
204 // TODO(kalman): I don't understand this code. How can type be "null"? | |
205 if (schema.optional && (type == "null" || type == "undefined")) | |
206 return true; | |
207 | |
208 var schemaTypes = this.getAllTypesForSchema(schema); | |
209 for (var i = 0; i < schemaTypes.length; i++) { | |
210 if (schemaTypes[i] == "any" || type == schemaTypes[i] || | |
211 (type == "integer" && schemaTypes[i] == "number")) | |
212 return true; | |
213 } | |
214 | |
215 return false; | |
216 }; | |
217 | |
218 /** | |
219 * Returns true if there is a non-null argument that both |schema1| and | |
220 * |schema2| would accept. | |
221 */ | |
222 JSONSchemaValidator.prototype.checkSchemaOverlap = function(schema1, schema2) { | |
223 var schema1Types = this.getAllTypesForSchema(schema1); | |
224 for (var i = 0; i < schema1Types.length; i++) { | |
225 if (this.isValidSchemaType(schema1Types[i], schema2)) | |
226 return true; | |
227 } | |
228 return false; | |
229 }; | |
230 | |
231 /** | |
232 * Validates an instance against a schema. The instance can be any JavaScript | |
233 * value and will be validated recursively. When this method returns, the | |
234 * |errors| property will contain a list of errors, if any. | |
235 */ | |
236 JSONSchemaValidator.prototype.validate = function(instance, schema, opt_path) { | |
237 var path = opt_path || ""; | |
238 | |
239 if (!schema) { | |
240 this.addError(path, "schemaRequired"); | |
241 return; | |
242 } | |
243 | |
244 // If this schema defines itself as reference type, save it in this.types. | |
245 if (schema.id) | |
246 this.types[schema.id] = schema; | |
247 | |
248 // If the schema has an extends property, the instance must validate against | |
249 // that schema too. | |
250 if (schema.extends) | |
251 this.validate(instance, schema.extends, path); | |
252 | |
253 // If the schema has a $ref property, the instance must validate against | |
254 // that schema too. It must be present in this.types to be referenced. | |
255 var ref = schema["$ref"]; | |
256 if (ref) { | |
257 if (!this.getOrAddType(ref)) | |
258 this.addError(path, "unknownSchemaReference", [ ref ]); | |
259 else | |
260 this.validate(instance, this.getOrAddType(ref), path) | |
261 } | |
262 | |
263 // If the schema has a choices property, the instance must validate against at | |
264 // least one of the items in that array. | |
265 if (schema.choices) { | |
266 this.validateChoices(instance, schema, path); | |
267 return; | |
268 } | |
269 | |
270 // If the schema has an enum property, the instance must be one of those | |
271 // values. | |
272 if (schema.enum) { | |
273 if (!this.validateEnum(instance, schema, path)) | |
274 return; | |
275 } | |
276 | |
277 if (schema.type && schema.type != "any") { | |
278 if (!this.validateType(instance, schema, path)) | |
279 return; | |
280 | |
281 // Type-specific validation. | |
282 switch (schema.type) { | |
283 case "object": | |
284 this.validateObject(instance, schema, path); | |
285 break; | |
286 case "array": | |
287 this.validateArray(instance, schema, path); | |
288 break; | |
289 case "string": | |
290 this.validateString(instance, schema, path); | |
291 break; | |
292 case "number": | |
293 case "integer": | |
294 this.validateNumber(instance, schema, path); | |
295 break; | |
296 } | |
297 } | |
298 }; | |
299 | |
300 /** | |
301 * Validates an instance against a choices schema. The instance must match at | |
302 * least one of the provided choices. | |
303 */ | |
304 JSONSchemaValidator.prototype.validateChoices = | |
305 function(instance, schema, path) { | |
306 var originalErrors = this.errors; | |
307 | |
308 for (var i = 0; i < schema.choices.length; i++) { | |
309 this.errors = []; | |
310 this.validate(instance, schema.choices[i], path); | |
311 if (this.errors.length == 0) { | |
312 this.errors = originalErrors; | |
313 return; | |
314 } | |
315 } | |
316 | |
317 this.errors = originalErrors; | |
318 this.addError(path, "invalidChoice"); | |
319 }; | |
320 | |
321 /** | |
322 * Validates an instance against a schema with an enum type. Populates the | |
323 * |errors| property, and returns a boolean indicating whether the instance | |
324 * validates. | |
325 */ | |
326 JSONSchemaValidator.prototype.validateEnum = function(instance, schema, path) { | |
327 for (var i = 0; i < schema.enum.length; i++) { | |
328 if (instance === enumToString(schema.enum[i])) | |
329 return true; | |
330 } | |
331 | |
332 this.addError(path, "invalidEnum", | |
333 [schema.enum.map(enumToString).join(", ")]); | |
334 return false; | |
335 }; | |
336 | |
337 /** | |
338 * Validates an instance against an object schema and populates the errors | |
339 * property. | |
340 */ | |
341 JSONSchemaValidator.prototype.validateObject = | |
342 function(instance, schema, path) { | |
343 if (schema.properties) { | |
344 for (var prop in schema.properties) { | |
345 // It is common in JavaScript to add properties to Object.prototype. This | |
346 // check prevents such additions from being interpreted as required | |
347 // schema properties. | |
348 // TODO(aa): If it ever turns out that we actually want this to work, | |
349 // there are other checks we could put here, like requiring that schema | |
350 // properties be objects that have a 'type' property. | |
351 if (!$Object.hasOwnProperty(schema.properties, prop)) | |
352 continue; | |
353 | |
354 var propPath = path ? path + "." + prop : prop; | |
355 if (schema.properties[prop] == undefined) { | |
356 this.addError(propPath, "invalidPropertyType"); | |
357 } else if (prop in instance && !isOptionalValue(instance[prop])) { | |
358 this.validate(instance[prop], schema.properties[prop], propPath); | |
359 } else if (!schema.properties[prop].optional) { | |
360 this.addError(propPath, "propertyRequired"); | |
361 } | |
362 } | |
363 } | |
364 | |
365 // If "instanceof" property is set, check that this object inherits from | |
366 // the specified constructor (function). | |
367 if (schema.isInstanceOf) { | |
368 if (!isInstanceOfClass(instance, schema.isInstanceOf)) | |
369 this.addError(propPath, "notInstance", [schema.isInstanceOf]); | |
370 } | |
371 | |
372 // Exit early from additional property check if "type":"any" is defined. | |
373 if (schema.additionalProperties && | |
374 schema.additionalProperties.type && | |
375 schema.additionalProperties.type == "any") { | |
376 return; | |
377 } | |
378 | |
379 // By default, additional properties are not allowed on instance objects. This | |
380 // can be overridden by setting the additionalProperties property to a schema | |
381 // which any additional properties must validate against. | |
382 for (var prop in instance) { | |
383 if (schema.properties && prop in schema.properties) | |
384 continue; | |
385 | |
386 // Any properties inherited through the prototype are ignored. | |
387 if (!$Object.hasOwnProperty(instance, prop)) | |
388 continue; | |
389 | |
390 var propPath = path ? path + "." + prop : prop; | |
391 if (schema.additionalProperties) | |
392 this.validate(instance[prop], schema.additionalProperties, propPath); | |
393 else | |
394 this.addError(propPath, "unexpectedProperty"); | |
395 } | |
396 }; | |
397 | |
398 /** | |
399 * Validates an instance against an array schema and populates the errors | |
400 * property. | |
401 */ | |
402 JSONSchemaValidator.prototype.validateArray = function(instance, schema, path) { | |
403 var typeOfItems = JSONSchemaValidator.getType(schema.items); | |
404 | |
405 if (typeOfItems == 'object') { | |
406 if (schema.minItems && instance.length < schema.minItems) { | |
407 this.addError(path, "arrayMinItems", [schema.minItems]); | |
408 } | |
409 | |
410 if (typeof schema.maxItems != "undefined" && | |
411 instance.length > schema.maxItems) { | |
412 this.addError(path, "arrayMaxItems", [schema.maxItems]); | |
413 } | |
414 | |
415 // If the items property is a single schema, each item in the array must | |
416 // have that schema. | |
417 for (var i = 0; i < instance.length; i++) { | |
418 this.validate(instance[i], schema.items, path + "." + i); | |
419 } | |
420 } else if (typeOfItems == 'array') { | |
421 // If the items property is an array of schemas, each item in the array must | |
422 // validate against the corresponding schema. | |
423 for (var i = 0; i < schema.items.length; i++) { | |
424 var itemPath = path ? path + "." + i : String(i); | |
425 if (i in instance && !isOptionalValue(instance[i])) { | |
426 this.validate(instance[i], schema.items[i], itemPath); | |
427 } else if (!schema.items[i].optional) { | |
428 this.addError(itemPath, "itemRequired"); | |
429 } | |
430 } | |
431 | |
432 if (schema.additionalProperties) { | |
433 for (var i = schema.items.length; i < instance.length; i++) { | |
434 var itemPath = path ? path + "." + i : String(i); | |
435 this.validate(instance[i], schema.additionalProperties, itemPath); | |
436 } | |
437 } else { | |
438 if (instance.length > schema.items.length) { | |
439 this.addError(path, "arrayMaxItems", [schema.items.length]); | |
440 } | |
441 } | |
442 } | |
443 }; | |
444 | |
445 /** | |
446 * Validates a string and populates the errors property. | |
447 */ | |
448 JSONSchemaValidator.prototype.validateString = | |
449 function(instance, schema, path) { | |
450 if (schema.minLength && instance.length < schema.minLength) | |
451 this.addError(path, "stringMinLength", [schema.minLength]); | |
452 | |
453 if (schema.maxLength && instance.length > schema.maxLength) | |
454 this.addError(path, "stringMaxLength", [schema.maxLength]); | |
455 | |
456 if (schema.pattern && !schema.pattern.test(instance)) | |
457 this.addError(path, "stringPattern", [schema.pattern]); | |
458 }; | |
459 | |
460 /** | |
461 * Validates a number and populates the errors property. The instance is | |
462 * assumed to be a number. | |
463 */ | |
464 JSONSchemaValidator.prototype.validateNumber = | |
465 function(instance, schema, path) { | |
466 // Forbid NaN, +Infinity, and -Infinity. Our APIs don't use them, and | |
467 // JSON serialization encodes them as 'null'. Re-evaluate supporting | |
468 // them if we add an API that could reasonably take them as a parameter. | |
469 if (isNaN(instance) || | |
470 instance == Number.POSITIVE_INFINITY || | |
471 instance == Number.NEGATIVE_INFINITY ) | |
472 this.addError(path, "numberFiniteNotNan", [instance]); | |
473 | |
474 if (schema.minimum !== undefined && instance < schema.minimum) | |
475 this.addError(path, "numberMinValue", [schema.minimum]); | |
476 | |
477 if (schema.maximum !== undefined && instance > schema.maximum) | |
478 this.addError(path, "numberMaxValue", [schema.maximum]); | |
479 | |
480 // Check for integer values outside of -2^31..2^31-1. | |
481 if (schema.type === "integer" && (instance | 0) !== instance) | |
482 this.addError(path, "numberIntValue", []); | |
483 | |
484 if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1) | |
485 this.addError(path, "numberMaxDecimal", [schema.maxDecimal]); | |
486 }; | |
487 | |
488 /** | |
489 * Validates the primitive type of an instance and populates the errors | |
490 * property. Returns true if the instance validates, false otherwise. | |
491 */ | |
492 JSONSchemaValidator.prototype.validateType = function(instance, schema, path) { | |
493 var actualType = JSONSchemaValidator.getType(instance); | |
494 if (schema.type == actualType || | |
495 (schema.type == "number" && actualType == "integer")) { | |
496 return true; | |
497 } else if (schema.type == "integer" && actualType == "number") { | |
498 this.addError(path, "invalidTypeIntegerNumber"); | |
499 return false; | |
500 } else { | |
501 this.addError(path, "invalidType", [schema.type, actualType]); | |
502 return false; | |
503 } | |
504 }; | |
505 | |
506 /** | |
507 * Adds an error message. |key| is an index into the |messages| object. | |
508 * |replacements| is an array of values to replace '*' characters in the | |
509 * message. | |
510 */ | |
511 JSONSchemaValidator.prototype.addError = function(path, key, replacements) { | |
512 $Array.push(this.errors, { | |
513 path: path, | |
514 message: JSONSchemaValidator.formatError(key, replacements) | |
515 }); | |
516 }; | |
517 | |
518 /** | |
519 * Resets errors to an empty list so you can call 'validate' again. | |
520 */ | |
521 JSONSchemaValidator.prototype.resetErrors = function() { | |
522 this.errors = []; | |
523 }; | |
524 | |
525 exports.JSONSchemaValidator = JSONSchemaValidator; | |
OLD | NEW |