OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 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 (function() { | |
42 native function GetChromeHidden(); | |
43 var chromeHidden = GetChromeHidden(); | |
44 | |
45 /** | |
46 * Validates an instance against a schema and accumulates errors. Usage: | |
47 * | |
48 * var validator = new chromeHidden.JSONSchemaValidator(); | |
49 * validator.validate(inst, schema); | |
50 * if (validator.errors.length == 0) | |
51 * console.log("Valid!"); | |
52 * else | |
53 * console.log(validator.errors); | |
54 * | |
55 * The errors property contains a list of objects. Each object has two | |
56 * properties: "path" and "message". The "path" property contains the path to | |
57 * the key that had the problem, and the "message" property contains a sentence | |
58 * describing the error. | |
59 */ | |
60 chromeHidden.JSONSchemaValidator = function() { | |
61 this.errors = []; | |
62 this.types = []; | |
63 }; | |
64 | |
65 chromeHidden.JSONSchemaValidator.messages = { | |
66 invalidEnum: "Value must be one of: [*].", | |
67 propertyRequired: "Property is required.", | |
68 unexpectedProperty: "Unexpected property.", | |
69 arrayMinItems: "Array must have at least * items.", | |
70 arrayMaxItems: "Array must not have more than * items.", | |
71 itemRequired: "Item is required.", | |
72 stringMinLength: "String must be at least * characters long.", | |
73 stringMaxLength: "String must not be more than * characters long.", | |
74 stringPattern: "String must match the pattern: *.", | |
75 numberFiniteNotNan: "Value must not be *.", | |
76 numberMinValue: "Value must not be less than *.", | |
77 numberMaxValue: "Value must not be greater than *.", | |
78 numberIntValue: "Value must fit in a 32-bit signed integer.", | |
79 numberMaxDecimal: "Value must not have more than * decimal places.", | |
80 invalidType: "Expected '*' but got '*'.", | |
81 invalidChoice: "Value does not match any valid type choices.", | |
82 invalidPropertyType: "Missing property type.", | |
83 schemaRequired: "Schema value required.", | |
84 unknownSchemaReference: "Unknown schema reference: *.", | |
85 notInstance: "Object must be an instance of *." | |
86 }; | |
87 | |
88 /** | |
89 * Builds an error message. Key is the property in the |errors| object, and | |
90 * |opt_replacements| is an array of values to replace "*" characters with. | |
91 */ | |
92 chromeHidden.JSONSchemaValidator.formatError = function(key, opt_replacements) { | |
93 var message = this.messages[key]; | |
94 if (opt_replacements) { | |
95 for (var i = 0; i < opt_replacements.length; i++) { | |
96 message = message.replace("*", opt_replacements[i]); | |
97 } | |
98 } | |
99 return message; | |
100 }; | |
101 | |
102 /** | |
103 * Classifies a value as one of the JSON schema primitive types. Note that we | |
104 * don't explicitly disallow 'function', because we want to allow functions in | |
105 * the input values. | |
106 */ | |
107 chromeHidden.JSONSchemaValidator.getType = function(value) { | |
108 var s = typeof value; | |
109 | |
110 if (s == "object") { | |
111 if (value === null) { | |
112 return "null"; | |
113 } else if (Object.prototype.toString.call(value) == "[object Array]") { | |
114 return "array"; | |
115 } | |
116 } else if (s == "number") { | |
117 if (value % 1 == 0) { | |
118 return "integer"; | |
119 } | |
120 } | |
121 | |
122 return s; | |
123 }; | |
124 | |
125 /** | |
126 * Add types that may be referenced by validated schemas that reference them | |
127 * with "$ref": <typeId>. Each type must be a valid schema and define an | |
128 * "id" property. | |
129 */ | |
130 chromeHidden.JSONSchemaValidator.prototype.addTypes = function(typeOrTypeList) { | |
131 function addType(validator, type) { | |
132 if(!type.id) | |
133 throw "Attempt to addType with missing 'id' property"; | |
134 validator.types[type.id] = type; | |
135 } | |
136 | |
137 if (typeOrTypeList instanceof Array) { | |
138 for (var i = 0; i < typeOrTypeList.length; i++) { | |
139 addType(this, typeOrTypeList[i]); | |
140 } | |
141 } else { | |
142 addType(this, typeOrTypeList); | |
143 } | |
144 } | |
145 | |
146 /** | |
147 * Validates an instance against a schema. The instance can be any JavaScript | |
148 * value and will be validated recursively. When this method returns, the | |
149 * |errors| property will contain a list of errors, if any. | |
150 */ | |
151 chromeHidden.JSONSchemaValidator.prototype.validate = function( | |
152 instance, schema, opt_path) { | |
153 var path = opt_path || ""; | |
154 | |
155 if (!schema) { | |
156 this.addError(path, "schemaRequired"); | |
157 return; | |
158 } | |
159 | |
160 // If this schema defines itself as reference type, save it in this.types. | |
161 if (schema.id) | |
162 this.types[schema.id] = schema; | |
163 | |
164 // If the schema has an extends property, the instance must validate against | |
165 // that schema too. | |
166 if (schema.extends) | |
167 this.validate(instance, schema.extends, path); | |
168 | |
169 // If the schema has a $ref property, the instance must validate against | |
170 // that schema too. It must be present in this.types to be referenced. | |
171 if (schema["$ref"]) { | |
172 if (!this.types[schema["$ref"]]) | |
173 this.addError(path, "unknownSchemaReference", [ schema["$ref"] ]); | |
174 else | |
175 this.validate(instance, this.types[schema["$ref"]], path) | |
176 } | |
177 | |
178 // If the schema has a choices property, the instance must validate against at | |
179 // least one of the items in that array. | |
180 if (schema.choices) { | |
181 this.validateChoices(instance, schema, path); | |
182 return; | |
183 } | |
184 | |
185 // If the schema has an enum property, the instance must be one of those | |
186 // values. | |
187 if (schema.enum) { | |
188 if (!this.validateEnum(instance, schema, path)) | |
189 return; | |
190 } | |
191 | |
192 if (schema.type && schema.type != "any") { | |
193 if (!this.validateType(instance, schema, path)) | |
194 return; | |
195 | |
196 // Type-specific validation. | |
197 switch (schema.type) { | |
198 case "object": | |
199 this.validateObject(instance, schema, path); | |
200 break; | |
201 case "array": | |
202 this.validateArray(instance, schema, path); | |
203 break; | |
204 case "string": | |
205 this.validateString(instance, schema, path); | |
206 break; | |
207 case "number": | |
208 case "integer": | |
209 this.validateNumber(instance, schema, path); | |
210 break; | |
211 } | |
212 } | |
213 }; | |
214 | |
215 /** | |
216 * Validates an instance against a choices schema. The instance must match at | |
217 * least one of the provided choices. | |
218 */ | |
219 chromeHidden.JSONSchemaValidator.prototype.validateChoices = function( | |
220 instance, schema, path) { | |
221 var originalErrors = this.errors; | |
222 | |
223 for (var i = 0; i < schema.choices.length; i++) { | |
224 this.errors = []; | |
225 this.validate(instance, schema.choices[i], path); | |
226 if (this.errors.length == 0) { | |
227 this.errors = originalErrors; | |
228 return; | |
229 } | |
230 } | |
231 | |
232 this.errors = originalErrors; | |
233 this.addError(path, "invalidChoice"); | |
234 }; | |
235 | |
236 /** | |
237 * Validates an instance against a schema with an enum type. Populates the | |
238 * |errors| property, and returns a boolean indicating whether the instance | |
239 * validates. | |
240 */ | |
241 chromeHidden.JSONSchemaValidator.prototype.validateEnum = function( | |
242 instance, schema, path) { | |
243 for (var i = 0; i < schema.enum.length; i++) { | |
244 if (instance === schema.enum[i]) | |
245 return true; | |
246 } | |
247 | |
248 this.addError(path, "invalidEnum", [schema.enum.join(", ")]); | |
249 return false; | |
250 }; | |
251 | |
252 /** | |
253 * Validates an instance against an object schema and populates the errors | |
254 * property. | |
255 */ | |
256 chromeHidden.JSONSchemaValidator.prototype.validateObject = function( | |
257 instance, schema, path) { | |
258 for (var prop in schema.properties) { | |
259 // It is common in JavaScript to add properties to Object.prototype. This | |
260 // check prevents such additions from being interpreted as required schema | |
261 // properties. | |
262 // TODO(aa): If it ever turns out that we actually want this to work, there | |
263 // are other checks we could put here, like requiring that schema properties | |
264 // be objects that have a 'type' property. | |
265 if (!schema.properties.hasOwnProperty(prop)) | |
266 continue; | |
267 | |
268 var propPath = path ? path + "." + prop : prop; | |
269 if (schema.properties[prop] == undefined) { | |
270 this.addError(propPath, "invalidPropertyType"); | |
271 } else if (prop in instance && instance[prop] !== undefined) { | |
272 this.validate(instance[prop], schema.properties[prop], propPath); | |
273 } else if (!schema.properties[prop].optional) { | |
274 this.addError(propPath, "propertyRequired"); | |
275 } | |
276 } | |
277 | |
278 // If "instanceof" property is set, check that this object inherits from | |
279 // the specified constructor (function). | |
280 if (schema.isInstanceOf) { | |
281 var isInstance = function() { | |
282 var constructor = this[schema.isInstanceOf]; | |
283 if (constructor) { | |
284 return (instance instanceof constructor); | |
285 } | |
286 | |
287 // Special-case constructors that can not always be found on the global | |
288 // object, but for which we to allow validation. | |
289 var allowedNamedConstructors = { | |
290 "DOMWindow": true, | |
291 "ImageData": true | |
292 } | |
293 if (!allowedNamedConstructors[schema.isInstanceOf]) { | |
294 throw "Attempt to validate against an instance ctor that could not be" + | |
295 "found: " + schema.isInstanceOf; | |
296 } | |
297 return (schema.isInstanceOf == instance.constructor.name) | |
298 }(); | |
299 | |
300 if (!isInstance) | |
301 this.addError(propPath, "notInstance", [schema.isInstanceOf]); | |
302 } | |
303 | |
304 // Exit early from additional property check if "type":"any" is defined. | |
305 if (schema.additionalProperties && | |
306 schema.additionalProperties.type && | |
307 schema.additionalProperties.type == "any") { | |
308 return; | |
309 } | |
310 | |
311 // By default, additional properties are not allowed on instance objects. This | |
312 // can be overridden by setting the additionalProperties property to a schema | |
313 // which any additional properties must validate against. | |
314 for (var prop in instance) { | |
315 if (prop in schema.properties) | |
316 continue; | |
317 | |
318 // Any properties inherited through the prototype are ignored. | |
319 if (!instance.hasOwnProperty(prop)) | |
320 continue; | |
321 | |
322 var propPath = path ? path + "." + prop : prop; | |
323 if (schema.additionalProperties) | |
324 this.validate(instance[prop], schema.additionalProperties, propPath); | |
325 else | |
326 this.addError(propPath, "unexpectedProperty"); | |
327 } | |
328 }; | |
329 | |
330 /** | |
331 * Validates an instance against an array schema and populates the errors | |
332 * property. | |
333 */ | |
334 chromeHidden.JSONSchemaValidator.prototype.validateArray = function( | |
335 instance, schema, path) { | |
336 var typeOfItems = chromeHidden.JSONSchemaValidator.getType(schema.items); | |
337 | |
338 if (typeOfItems == 'object') { | |
339 if (schema.minItems && instance.length < schema.minItems) { | |
340 this.addError(path, "arrayMinItems", [schema.minItems]); | |
341 } | |
342 | |
343 if (typeof schema.maxItems != "undefined" && | |
344 instance.length > schema.maxItems) { | |
345 this.addError(path, "arrayMaxItems", [schema.maxItems]); | |
346 } | |
347 | |
348 // If the items property is a single schema, each item in the array must | |
349 // have that schema. | |
350 for (var i = 0; i < instance.length; i++) { | |
351 this.validate(instance[i], schema.items, path + "." + i); | |
352 } | |
353 } else if (typeOfItems == 'array') { | |
354 // If the items property is an array of schemas, each item in the array must | |
355 // validate against the corresponding schema. | |
356 for (var i = 0; i < schema.items.length; i++) { | |
357 var itemPath = path ? path + "." + i : String(i); | |
358 if (i in instance && instance[i] !== null && instance[i] !== undefined) { | |
359 this.validate(instance[i], schema.items[i], itemPath); | |
360 } else if (!schema.items[i].optional) { | |
361 this.addError(itemPath, "itemRequired"); | |
362 } | |
363 } | |
364 | |
365 if (schema.additionalProperties) { | |
366 for (var i = schema.items.length; i < instance.length; i++) { | |
367 var itemPath = path ? path + "." + i : String(i); | |
368 this.validate(instance[i], schema.additionalProperties, itemPath); | |
369 } | |
370 } else { | |
371 if (instance.length > schema.items.length) { | |
372 this.addError(path, "arrayMaxItems", [schema.items.length]); | |
373 } | |
374 } | |
375 } | |
376 }; | |
377 | |
378 /** | |
379 * Validates a string and populates the errors property. | |
380 */ | |
381 chromeHidden.JSONSchemaValidator.prototype.validateString = function( | |
382 instance, schema, path) { | |
383 if (schema.minLength && instance.length < schema.minLength) | |
384 this.addError(path, "stringMinLength", [schema.minLength]); | |
385 | |
386 if (schema.maxLength && instance.length > schema.maxLength) | |
387 this.addError(path, "stringMaxLength", [schema.maxLength]); | |
388 | |
389 if (schema.pattern && !schema.pattern.test(instance)) | |
390 this.addError(path, "stringPattern", [schema.pattern]); | |
391 }; | |
392 | |
393 /** | |
394 * Validates a number and populates the errors property. The instance is | |
395 * assumed to be a number. | |
396 */ | |
397 chromeHidden.JSONSchemaValidator.prototype.validateNumber = function( | |
398 instance, schema, path) { | |
399 | |
400 // Forbid NaN, +Infinity, and -Infinity. Our APIs don't use them, and | |
401 // JSON serialization encodes them as 'null'. Re-evaluate supporting | |
402 // them if we add an API that could reasonably take them as a parameter. | |
403 if (isNaN(instance) || | |
404 instance == Number.POSITIVE_INFINITY || | |
405 instance == Number.NEGATIVE_INFINITY ) | |
406 this.addError(path, "numberFiniteNotNan", [instance]); | |
407 | |
408 if (schema.minimum !== undefined && instance < schema.minimum) | |
409 this.addError(path, "numberMinValue", [schema.minimum]); | |
410 | |
411 if (schema.maximum !== undefined && instance > schema.maximum) | |
412 this.addError(path, "numberMaxValue", [schema.maximum]); | |
413 | |
414 // Check for integer values outside of -2^31..2^31-1. | |
415 if (schema.type === "integer" && (instance | 0) !== instance) | |
416 this.addError(path, "numberIntValue", []); | |
417 | |
418 if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1) | |
419 this.addError(path, "numberMaxDecimal", [schema.maxDecimal]); | |
420 }; | |
421 | |
422 /** | |
423 * Validates the primitive type of an instance and populates the errors | |
424 * property. Returns true if the instance validates, false otherwise. | |
425 */ | |
426 chromeHidden.JSONSchemaValidator.prototype.validateType = function( | |
427 instance, schema, path) { | |
428 var actualType = chromeHidden.JSONSchemaValidator.getType(instance); | |
429 if (schema.type != actualType && !(schema.type == "number" && | |
430 actualType == "integer")) { | |
431 this.addError(path, "invalidType", [schema.type, actualType]); | |
432 return false; | |
433 } | |
434 | |
435 return true; | |
436 }; | |
437 | |
438 /** | |
439 * Adds an error message. |key| is an index into the |messages| object. | |
440 * |replacements| is an array of values to replace '*' characters in the | |
441 * message. | |
442 */ | |
443 chromeHidden.JSONSchemaValidator.prototype.addError = function( | |
444 path, key, replacements) { | |
445 this.errors.push({ | |
446 path: path, | |
447 message: chromeHidden.JSONSchemaValidator.formatError(key, replacements) | |
448 }); | |
449 }; | |
450 | |
451 })(); | |
OLD | NEW |