- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / renderer / resources / extensions / json_schema.js
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       return true;
212   }
213
214   return false;
215 };
216
217 /**
218  * Returns true if there is a non-null argument that both |schema1| and
219  * |schema2| would accept.
220  */
221 JSONSchemaValidator.prototype.checkSchemaOverlap = function(schema1, schema2) {
222   var schema1Types = this.getAllTypesForSchema(schema1);
223   for (var i = 0; i < schema1Types.length; i++) {
224     if (this.isValidSchemaType(schema1Types[i], schema2))
225       return true;
226   }
227   return false;
228 };
229
230 /**
231  * Validates an instance against a schema. The instance can be any JavaScript
232  * value and will be validated recursively. When this method returns, the
233  * |errors| property will contain a list of errors, if any.
234  */
235 JSONSchemaValidator.prototype.validate = function(instance, schema, opt_path) {
236   var path = opt_path || "";
237
238   if (!schema) {
239     this.addError(path, "schemaRequired");
240     return;
241   }
242
243   // If this schema defines itself as reference type, save it in this.types.
244   if (schema.id)
245     this.types[schema.id] = schema;
246
247   // If the schema has an extends property, the instance must validate against
248   // that schema too.
249   if (schema.extends)
250     this.validate(instance, schema.extends, path);
251
252   // If the schema has a $ref property, the instance must validate against
253   // that schema too. It must be present in this.types to be referenced.
254   var ref = schema["$ref"];
255   if (ref) {
256     if (!this.getOrAddType(ref))
257       this.addError(path, "unknownSchemaReference", [ ref ]);
258     else
259       this.validate(instance, this.getOrAddType(ref), path)
260   }
261
262   // If the schema has a choices property, the instance must validate against at
263   // least one of the items in that array.
264   if (schema.choices) {
265     this.validateChoices(instance, schema, path);
266     return;
267   }
268
269   // If the schema has an enum property, the instance must be one of those
270   // values.
271   if (schema.enum) {
272     if (!this.validateEnum(instance, schema, path))
273       return;
274   }
275
276   if (schema.type && schema.type != "any") {
277     if (!this.validateType(instance, schema, path))
278       return;
279
280     // Type-specific validation.
281     switch (schema.type) {
282       case "object":
283         this.validateObject(instance, schema, path);
284         break;
285       case "array":
286         this.validateArray(instance, schema, path);
287         break;
288       case "string":
289         this.validateString(instance, schema, path);
290         break;
291       case "number":
292       case "integer":
293         this.validateNumber(instance, schema, path);
294         break;
295     }
296   }
297 };
298
299 /**
300  * Validates an instance against a choices schema. The instance must match at
301  * least one of the provided choices.
302  */
303 JSONSchemaValidator.prototype.validateChoices =
304     function(instance, schema, path) {
305   var originalErrors = this.errors;
306
307   for (var i = 0; i < schema.choices.length; i++) {
308     this.errors = [];
309     this.validate(instance, schema.choices[i], path);
310     if (this.errors.length == 0) {
311       this.errors = originalErrors;
312       return;
313     }
314   }
315
316   this.errors = originalErrors;
317   this.addError(path, "invalidChoice");
318 };
319
320 /**
321  * Validates an instance against a schema with an enum type. Populates the
322  * |errors| property, and returns a boolean indicating whether the instance
323  * validates.
324  */
325 JSONSchemaValidator.prototype.validateEnum = function(instance, schema, path) {
326   for (var i = 0; i < schema.enum.length; i++) {
327     if (instance === enumToString(schema.enum[i]))
328       return true;
329   }
330
331   this.addError(path, "invalidEnum",
332                 [schema.enum.map(enumToString).join(", ")]);
333   return false;
334 };
335
336 /**
337  * Validates an instance against an object schema and populates the errors
338  * property.
339  */
340 JSONSchemaValidator.prototype.validateObject =
341     function(instance, schema, path) {
342   if (schema.properties) {
343     for (var prop in schema.properties) {
344       // It is common in JavaScript to add properties to Object.prototype. This
345       // check prevents such additions from being interpreted as required
346       // schema properties.
347       // TODO(aa): If it ever turns out that we actually want this to work,
348       // there are other checks we could put here, like requiring that schema
349       // properties be objects that have a 'type' property.
350       if (!$Object.hasOwnProperty(schema.properties, prop))
351         continue;
352
353       var propPath = path ? path + "." + prop : prop;
354       if (schema.properties[prop] == undefined) {
355         this.addError(propPath, "invalidPropertyType");
356       } else if (prop in instance && !isOptionalValue(instance[prop])) {
357         this.validate(instance[prop], schema.properties[prop], propPath);
358       } else if (!schema.properties[prop].optional) {
359         this.addError(propPath, "propertyRequired");
360       }
361     }
362   }
363
364   // If "instanceof" property is set, check that this object inherits from
365   // the specified constructor (function).
366   if (schema.isInstanceOf) {
367     if (!isInstanceOfClass(instance, schema.isInstanceOf))
368       this.addError(propPath, "notInstance", [schema.isInstanceOf]);
369   }
370
371   // Exit early from additional property check if "type":"any" is defined.
372   if (schema.additionalProperties &&
373       schema.additionalProperties.type &&
374       schema.additionalProperties.type == "any") {
375     return;
376   }
377
378   // By default, additional properties are not allowed on instance objects. This
379   // can be overridden by setting the additionalProperties property to a schema
380   // which any additional properties must validate against.
381   for (var prop in instance) {
382     if (schema.properties && prop in schema.properties)
383       continue;
384
385     // Any properties inherited through the prototype are ignored.
386     if (!$Object.hasOwnProperty(instance, prop))
387       continue;
388
389     var propPath = path ? path + "." + prop : prop;
390     if (schema.additionalProperties)
391       this.validate(instance[prop], schema.additionalProperties, propPath);
392     else
393       this.addError(propPath, "unexpectedProperty");
394   }
395 };
396
397 /**
398  * Validates an instance against an array schema and populates the errors
399  * property.
400  */
401 JSONSchemaValidator.prototype.validateArray = function(instance, schema, path) {
402   var typeOfItems = JSONSchemaValidator.getType(schema.items);
403
404   if (typeOfItems == 'object') {
405     if (schema.minItems && instance.length < schema.minItems) {
406       this.addError(path, "arrayMinItems", [schema.minItems]);
407     }
408
409     if (typeof schema.maxItems != "undefined" &&
410         instance.length > schema.maxItems) {
411       this.addError(path, "arrayMaxItems", [schema.maxItems]);
412     }
413
414     // If the items property is a single schema, each item in the array must
415     // have that schema.
416     for (var i = 0; i < instance.length; i++) {
417       this.validate(instance[i], schema.items, path + "." + i);
418     }
419   } else if (typeOfItems == 'array') {
420     // If the items property is an array of schemas, each item in the array must
421     // validate against the corresponding schema.
422     for (var i = 0; i < schema.items.length; i++) {
423       var itemPath = path ? path + "." + i : String(i);
424       if (i in instance && !isOptionalValue(instance[i])) {
425         this.validate(instance[i], schema.items[i], itemPath);
426       } else if (!schema.items[i].optional) {
427         this.addError(itemPath, "itemRequired");
428       }
429     }
430
431     if (schema.additionalProperties) {
432       for (var i = schema.items.length; i < instance.length; i++) {
433         var itemPath = path ? path + "." + i : String(i);
434         this.validate(instance[i], schema.additionalProperties, itemPath);
435       }
436     } else {
437       if (instance.length > schema.items.length) {
438         this.addError(path, "arrayMaxItems", [schema.items.length]);
439       }
440     }
441   }
442 };
443
444 /**
445  * Validates a string and populates the errors property.
446  */
447 JSONSchemaValidator.prototype.validateString =
448     function(instance, schema, path) {
449   if (schema.minLength && instance.length < schema.minLength)
450     this.addError(path, "stringMinLength", [schema.minLength]);
451
452   if (schema.maxLength && instance.length > schema.maxLength)
453     this.addError(path, "stringMaxLength", [schema.maxLength]);
454
455   if (schema.pattern && !schema.pattern.test(instance))
456     this.addError(path, "stringPattern", [schema.pattern]);
457 };
458
459 /**
460  * Validates a number and populates the errors property. The instance is
461  * assumed to be a number.
462  */
463 JSONSchemaValidator.prototype.validateNumber =
464     function(instance, schema, path) {
465   // Forbid NaN, +Infinity, and -Infinity.  Our APIs don't use them, and
466   // JSON serialization encodes them as 'null'.  Re-evaluate supporting
467   // them if we add an API that could reasonably take them as a parameter.
468   if (isNaN(instance) ||
469       instance == Number.POSITIVE_INFINITY ||
470       instance == Number.NEGATIVE_INFINITY )
471     this.addError(path, "numberFiniteNotNan", [instance]);
472
473   if (schema.minimum !== undefined && instance < schema.minimum)
474     this.addError(path, "numberMinValue", [schema.minimum]);
475
476   if (schema.maximum !== undefined && instance > schema.maximum)
477     this.addError(path, "numberMaxValue", [schema.maximum]);
478
479   // Check for integer values outside of -2^31..2^31-1.
480   if (schema.type === "integer" && (instance | 0) !== instance)
481     this.addError(path, "numberIntValue", []);
482
483   if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1)
484     this.addError(path, "numberMaxDecimal", [schema.maxDecimal]);
485 };
486
487 /**
488  * Validates the primitive type of an instance and populates the errors
489  * property. Returns true if the instance validates, false otherwise.
490  */
491 JSONSchemaValidator.prototype.validateType = function(instance, schema, path) {
492   var actualType = JSONSchemaValidator.getType(instance);
493   if (schema.type == actualType ||
494       (schema.type == "number" && actualType == "integer")) {
495     return true;
496   } else if (schema.type == "integer" && actualType == "number") {
497     this.addError(path, "invalidTypeIntegerNumber");
498     return false;
499   } else {
500     this.addError(path, "invalidType", [schema.type, actualType]);
501     return false;
502   }
503 };
504
505 /**
506  * Adds an error message. |key| is an index into the |messages| object.
507  * |replacements| is an array of values to replace '*' characters in the
508  * message.
509  */
510 JSONSchemaValidator.prototype.addError = function(path, key, replacements) {
511   $Array.push(this.errors, {
512     path: path,
513     message: JSONSchemaValidator.formatError(key, replacements)
514   });
515 };
516
517 /**
518  * Resets errors to an empty list so you can call 'validate' again.
519  */
520 JSONSchemaValidator.prototype.resetErrors = function() {
521   this.errors = [];
522 };
523
524 exports.JSONSchemaValidator = JSONSchemaValidator;