/** * JSV: JSON Schema Validator * * @fileOverview A JavaScript implementation of a extendable, fully compliant JSON Schema validator. * @author Gary Court * @version 4.0.2 * @see http://github.com/garycourt/JSV */ /* * Copyright 2010 Gary Court. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY GARY COURT ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of Gary Court or the JSON Schema specification. */ /*jslint white: true, sub: true, onevar: true, undef: true, eqeqeq: true, newcap: true, immed: true, indent: 4 */ var exports = exports || this, require = require || function () { return exports; }; (function () { var URI = require("./uri/uri").URI, O = {}, I2H = "0123456789abcdef".split(""), mapArray, filterArray, searchArray, JSV; // // Utility functions // function typeOf(o) { return o === undefined ? "undefined" : (o === null ? "null" : Object.prototype.toString.call(o).split(" ").pop().split("]").shift().toLowerCase()); } /** @inner */ function F() {} function createObject(proto) { F.prototype = proto || {}; return new F(); } function mapObject(obj, func, scope) { var newObj = {}, key; for (key in obj) { if (obj[key] !== O[key]) { newObj[key] = func.call(scope, obj[key], key, obj); } } return newObj; } /** @ignore */ mapArray = function (arr, func, scope) { var x = 0, xl = arr.length, newArr = new Array(xl); for (; x < xl; ++x) { newArr[x] = func.call(scope, arr[x], x, arr); } return newArr; }; if (Array.prototype.map) { /** @ignore */ mapArray = function (arr, func, scope) { return Array.prototype.map.call(arr, func, scope); }; } /** @ignore */ filterArray = function (arr, func, scope) { var x = 0, xl = arr.length, newArr = []; for (; x < xl; ++x) { if (func.call(scope, arr[x], x, arr)) { newArr[newArr.length] = arr[x]; } } return newArr; }; if (Array.prototype.filter) { /** @ignore */ filterArray = function (arr, func, scope) { return Array.prototype.filter.call(arr, func, scope); }; } /** @ignore */ searchArray = function (arr, o) { var x = 0, xl = arr.length; for (; x < xl; ++x) { if (arr[x] === o) { return x; } } return -1; }; if (Array.prototype.indexOf) { /** @ignore */ searchArray = function (arr, o) { return Array.prototype.indexOf.call(arr, o); }; } function toArray(o) { return o !== undefined && o !== null ? (o instanceof Array && !o.callee ? o : (typeof o.length !== "number" || o.split || o.setInterval || o.call ? [ o ] : Array.prototype.slice.call(o))) : []; } function keys(o) { var result = [], key; switch (typeOf(o)) { case "object": for (key in o) { if (o[key] !== O[key]) { result[result.length] = key; } } break; case "array": for (key = o.length - 1; key >= 0; --key) { result[key] = key; } break; } return result; } function pushUnique(arr, o) { if (searchArray(arr, o) === -1) { arr.push(o); } return arr; } function popFirst(arr, o) { var index = searchArray(arr, o); if (index > -1) { arr.splice(index, 1); } return arr; } function randomUUID() { return [ I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], "-", I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], "-4", //set 4 high bits of time_high field to version I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], "-", I2H[(Math.floor(Math.random() * 0x10) & 0x3) | 0x8], //specify 2 high bits of clock sequence I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], "-", I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)], I2H[Math.floor(Math.random() * 0x10)] ].join(""); } function escapeURIComponent(str) { return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A'); } function formatURI(uri) { if (typeof uri === "string" && uri.indexOf("#") === -1) { uri += "#"; } return uri; } function stripInstances(o) { if (o instanceof JSONInstance) { return o.getURI(); } switch (typeOf(o)) { case "undefined": case "null": case "boolean": case "number": case "string": return o; //do nothing case "object": return mapObject(o, stripInstances); case "array": return mapArray(o, stripInstances); default: return o.toString(); } } /** * The exception that is thrown when a schema fails to be created. * * @name InitializationError * @class * @param {JSONInstance|String} instance The instance (or instance URI) that is invalid * @param {JSONSchema|String} schema The schema (or schema URI) that was validating the instance * @param {String} attr The attribute that failed to validated * @param {String} message A user-friendly message on why the schema attribute failed to validate the instance * @param {Any} details The value of the schema attribute */ function InitializationError(instance, schema, attr, message, details) { Error.call(this, message); this.uri = instance instanceof JSONInstance ? instance.getURI() : instance; this.schemaUri = schema instanceof JSONInstance ? schema.getURI() : schema; this.attribute = attr; this.message = message; this.description = message; //IE this.details = details; } InitializationError.prototype = new Error(); InitializationError.prototype.constructor = InitializationError; InitializationError.prototype.name = "InitializationError"; /** * Defines an error, found by a schema, with an instance. * This class can only be instantiated by {@link Report#addError}. * * @name ValidationError * @class * @see Report#addError */ /** * The URI of the instance that has the error. * * @name ValidationError.prototype.uri * @type String */ /** * The URI of the schema that generated the error. * * @name ValidationError.prototype.schemaUri * @type String */ /** * The name of the schema attribute that generated the error. * * @name ValidationError.prototype.attribute * @type String */ /** * An user-friendly (English) message about what failed to validate. * * @name ValidationError.prototype.message * @type String */ /** * The value of the schema attribute that generated the error. * * @name ValidationError.prototype.details * @type Any */ /** * Reports are returned from validation methods to describe the result of a validation. * * @name Report * @class * @see JSONSchema#validate * @see Environment#validate */ function Report() { /** * An array of {@link ValidationError} objects that define all the errors generated by the schema against the instance. * * @name Report.prototype.errors * @type Array * @see Report#addError */ this.errors = []; /** * A hash table of every instance and what schemas were validated against it. *

* The key of each item in the table is the URI of the instance that was validated. * The value of this key is an array of strings of URIs of the schema that validated it. *

* * @name Report.prototype.validated * @type Object * @see Report#registerValidation * @see Report#isValidatedBy */ this.validated = {}; /** * If the report is generated by {@link Environment#validate}, this field is the generated instance. * * @name Report.prototype.instance * @type JSONInstance * @see Environment#validate */ /** * If the report is generated by {@link Environment#validate}, this field is the generated schema. * * @name Report.prototype.schema * @type JSONSchema * @see Environment#validate */ /** * If the report is generated by {@link Environment#validate}, this field is the schema's schema. * This value is the same as calling schema.getSchema(). * * @name Report.prototype.schemaSchema * @type JSONSchema * @see Environment#validate * @see JSONSchema#getSchema */ } /** * Adds a {@link ValidationError} object to the errors field. * * @param {JSONInstance|String} instance The instance (or instance URI) that is invalid * @param {JSONSchema|String} schema The schema (or schema URI) that was validating the instance * @param {String} attr The attribute that failed to validated * @param {String} message A user-friendly message on why the schema attribute failed to validate the instance * @param {Any} details The value of the schema attribute */ Report.prototype.addError = function (instance, schema, attr, message, details) { this.errors.push({ uri : instance instanceof JSONInstance ? instance.getURI() : instance, schemaUri : schema instanceof JSONInstance ? schema.getURI() : schema, attribute : attr, message : message, details : stripInstances(details) }); }; /** * Registers that the provided instance URI has been validated by the provided schema URI. * This is recorded in the validated field. * * @param {String} uri The URI of the instance that was validated * @param {String} schemaUri The URI of the schema that validated the instance */ Report.prototype.registerValidation = function (uri, schemaUri) { if (!this.validated[uri]) { this.validated[uri] = [ schemaUri ]; } else { this.validated[uri].push(schemaUri); } }; /** * Returns if an instance with the provided URI has been validated by the schema with the provided URI. * * @param {String} uri The URI of the instance * @param {String} schemaUri The URI of a schema * @returns {Boolean} If the instance has been validated by the schema. */ Report.prototype.isValidatedBy = function (uri, schemaUri) { return !!this.validated[uri] && searchArray(this.validated[uri], schemaUri) !== -1; }; /** * A wrapper class for binding an Environment, URI and helper methods to an instance. * This class is most commonly instantiated with {@link Environment#createInstance}. * * @name JSONInstance * @class * @param {Environment} env The environment this instance belongs to * @param {JSONInstance|Any} json The value of the instance * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. * @param {String} [fd] The fragment delimiter for properties. If undefined, uses the environment default. */ function JSONInstance(env, json, uri, fd) { if (json instanceof JSONInstance) { if (typeof fd !== "string") { fd = json._fd; } if (typeof uri !== "string") { uri = json._uri; } json = json._value; } if (typeof uri !== "string") { uri = "urn:uuid:" + randomUUID() + "#"; } else if (uri.indexOf(":") === -1) { uri = formatURI(URI.resolve("urn:uuid:" + randomUUID() + "#", uri)); } this._env = env; this._value = json; this._uri = uri; this._fd = fd || this._env._options["defaultFragmentDelimiter"]; } /** * Returns the environment the instance is bound to. * * @returns {Environment} The environment of the instance */ JSONInstance.prototype.getEnvironment = function () { return this._env; }; /** * Returns the name of the type of the instance. * * @returns {String} The name of the type of the instance */ JSONInstance.prototype.getType = function () { return typeOf(this._value); }; /** * Returns the JSON value of the instance. * * @returns {Any} The actual JavaScript value of the instance */ JSONInstance.prototype.getValue = function () { return this._value; }; /** * Returns the URI of the instance. * * @returns {String} The URI of the instance */ JSONInstance.prototype.getURI = function () { return this._uri; }; /** * Returns a resolved URI of a provided relative URI against the URI of the instance. * * @param {String} uri The relative URI to resolve * @returns {String} The resolved URI */ JSONInstance.prototype.resolveURI = function (uri) { return formatURI(URI.resolve(this._uri, uri)); }; /** * Returns an array of the names of all the properties. * * @returns {Array} An array of strings which are the names of all the properties */ JSONInstance.prototype.getPropertyNames = function () { return keys(this._value); }; /** * Returns a {@link JSONInstance} of the value of the provided property name. * * @param {String} key The name of the property to fetch * @returns {JSONInstance} The instance of the property value */ JSONInstance.prototype.getProperty = function (key) { var value = this._value ? this._value[key] : undefined; if (value instanceof JSONInstance) { return value; } //else return new JSONInstance(this._env, value, this._uri + this._fd + escapeURIComponent(key), this._fd); }; /** * Returns all the property instances of the target instance. *

* If the target instance is an Object, then the method will return a hash table of {@link JSONInstance}s of all the properties. * If the target instance is an Array, then the method will return an array of {@link JSONInstance}s of all the items. *

* * @returns {Object|Array|undefined} The list of instances for all the properties */ JSONInstance.prototype.getProperties = function () { var type = typeOf(this._value), self = this; if (type === "object") { return mapObject(this._value, function (value, key) { if (value instanceof JSONInstance) { return value; } return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd); }); } else if (type === "array") { return mapArray(this._value, function (value, key) { if (value instanceof JSONInstance) { return value; } return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd); }); } }; /** * Returns the JSON value of the provided property name. * This method is a faster version of calling instance.getProperty(key).getValue(). * * @param {String} key The name of the property * @returns {Any} The JavaScript value of the instance * @see JSONInstance#getProperty * @see JSONInstance#getValue */ JSONInstance.prototype.getValueOfProperty = function (key) { if (this._value) { if (this._value[key] instanceof JSONInstance) { return this._value[key]._value; } return this._value[key]; } }; /** * Return if the provided value is the same as the value of the instance. * * @param {JSONInstance|Any} instance The value to compare * @returns {Boolean} If both the instance and the value match */ JSONInstance.prototype.equals = function (instance) { if (instance instanceof JSONInstance) { return this._value === instance._value; } //else return this._value === instance; }; /** * Warning: Not a generic clone function * Produces a JSV acceptable clone */ function clone(obj, deep) { var newObj, x; if (obj instanceof JSONInstance) { obj = obj.getValue(); } switch (typeOf(obj)) { case "object": if (deep) { newObj = {}; for (x in obj) { if (obj[x] !== O[x]) { newObj[x] = clone(obj[x], deep); } } return newObj; } else { return createObject(obj); } break; case "array": if (deep) { newObj = new Array(obj.length); x = obj.length; while (--x >= 0) { newObj[x] = clone(obj[x], deep); } return newObj; } else { return Array.prototype.slice.call(obj); } break; default: return obj; } } /** * This class binds a {@link JSONInstance} with a {@link JSONSchema} to provided context aware methods. * * @name JSONSchema * @class * @param {Environment} env The environment this schema belongs to * @param {JSONInstance|Any} json The value of the schema * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. * @param {JSONSchema|Boolean} [schema] The schema to bind to the instance. If undefined, the environment's default schema will be used. If true, the instance's schema will be itself. * @extends JSONInstance */ function JSONSchema(env, json, uri, schema) { var fr; JSONInstance.call(this, env, json, uri); if (schema === true) { this._schema = this; } else if (json instanceof JSONSchema && !(schema instanceof JSONSchema)) { this._schema = json._schema; //TODO: Make sure cross environments don't mess everything up } else { this._schema = schema instanceof JSONSchema ? schema : this._env.getDefaultSchema() || this._env.createEmptySchema(); } //determine fragment delimiter from schema fr = this._schema.getValueOfProperty("fragmentResolution"); if (fr === "dot-delimited") { this._fd = "."; } else if (fr === "slash-delimited") { this._fd = "/"; } return this.rebuild(); //this works even when called with "new" } JSONSchema.prototype = createObject(JSONInstance.prototype); /** * Returns the schema of the schema. * * @returns {JSONSchema} The schema of the schema */ JSONSchema.prototype.getSchema = function () { var uri = this._refs && this._refs["describedby"], newSchema; if (uri) { newSchema = uri && this._env.findSchema(uri); if (newSchema) { if (!newSchema.equals(this._schema)) { this._schema = newSchema; this.rebuild(); //if the schema has changed, the context has changed - so everything must be rebuilt } } else if (this._env._options["enforceReferences"]) { throw new InitializationError(this, this._schema, "{describedby}", "Unknown schema reference", uri); } } return this._schema; }; /** * Returns the value of the provided attribute name. *

* This method is different from {@link JSONInstance#getProperty} as the named property * is converted using a parser defined by the schema's schema before being returned. This * makes the return value of this method attribute dependent. *

* * @param {String} key The name of the attribute * @param {Any} [arg] Some attribute parsers accept special arguments for returning resolved values. This is attribute dependent. * @returns {JSONSchema|Any} The value of the attribute */ JSONSchema.prototype.getAttribute = function (key, arg) { var schemaProperty, parser, property, result, schema = this.getSchema(); //we do this here to make sure the "describedby" reference has not changed, and that the attribute cache is up-to-date if (!arg && this._attributes && this._attributes.hasOwnProperty(key)) { return this._attributes[key]; } schemaProperty = schema.getProperty("properties").getProperty(key); parser = schemaProperty.getValueOfProperty("parser"); property = this.getProperty(key); if (typeof parser === "function") { result = parser(property, schemaProperty, arg); if (!arg && this._attributes) { this._attributes[key] = result; } return result; } //else return property.getValue(); }; /** * Returns all the attributes of the schema. * * @returns {Object} A map of all parsed attribute values */ JSONSchema.prototype.getAttributes = function () { var properties, schemaProperties, key, schemaProperty, parser, schema = this.getSchema(); //we do this here to make sure the "describedby" reference has not changed, and that the attribute cache is up-to-date if (!this._attributes && this.getType() === "object") { properties = this.getProperties(); schemaProperties = schema.getProperty("properties"); this._attributes = {}; for (key in properties) { if (properties[key] !== O[key]) { schemaProperty = schemaProperties && schemaProperties.getProperty(key); parser = schemaProperty && schemaProperty.getValueOfProperty("parser"); if (typeof parser === "function") { this._attributes[key] = parser(properties[key], schemaProperty); } else { this._attributes[key] = properties[key].getValue(); } } } } return clone(this._attributes, false); }; /** * Convenience method for retrieving a link or link object from a schema. * This method is the same as calling schema.getAttribute("links", [rel, instance])[0];. * * @param {String} rel The link relationship * @param {JSONInstance} [instance] The instance to resolve any URIs from * @returns {String|Object|undefined} If instance is provided, a string containing the resolve URI of the link is returned. * If instance is not provided, a link object is returned with details of the link. * If no link with the provided relationship exists, undefined is returned. * @see JSONSchema#getAttribute */ JSONSchema.prototype.getLink = function (rel, instance) { var schemaLinks = this.getAttribute("links", [rel, instance]); if (schemaLinks && schemaLinks.length && schemaLinks[schemaLinks.length - 1]) { return schemaLinks[schemaLinks.length - 1]; } }; /** * Validates the provided instance against the target schema and returns a {@link Report}. * * @param {JSONInstance|Any} instance The instance to validate; may be a {@link JSONInstance} or any JavaScript value * @param {Report} [report] A {@link Report} to concatenate the result of the validation to. If undefined, a new {@link Report} is created. * @param {JSONInstance} [parent] The parent/containing instance of the provided instance * @param {JSONSchema} [parentSchema] The schema of the parent/containing instance * @param {String} [name] The name of the parent object's property that references the instance * @returns {Report} The result of the validation */ JSONSchema.prototype.validate = function (instance, report, parent, parentSchema, name) { var schemaSchema = this.getSchema(), validator = schemaSchema.getValueOfProperty("validator"); if (!(instance instanceof JSONInstance)) { instance = this.getEnvironment().createInstance(instance); } if (!(report instanceof Report)) { report = new Report(); } if (this._env._options["validateReferences"] && this._refs) { if (this._refs["describedby"] && !this._env.findSchema(this._refs["describedby"])) { report.addError(this, this._schema, "{describedby}", "Unknown schema reference", this._refs["describedby"]); } if (this._refs["full"] && !this._env.findSchema(this._refs["full"])) { report.addError(this, this._schema, "{full}", "Unknown schema reference", this._refs["full"]); } } if (typeof validator === "function" && !report.isValidatedBy(instance.getURI(), this.getURI())) { report.registerValidation(instance.getURI(), this.getURI()); validator(instance, this, schemaSchema, report, parent, parentSchema, name); } return report; }; /** @inner */ function createFullLookupWrapper(func) { return /** @inner */ function fullLookupWrapper() { var scope = this, stack = [], uri = scope._refs && scope._refs["full"], schema; while (uri) { schema = scope._env.findSchema(uri); if (schema) { if (schema._value === scope._value) { break; } scope = schema; stack.push(uri); uri = scope._refs && scope._refs["full"]; if (stack.indexOf(uri) > -1) { break; //stop infinite loop } } else if (scope._env._options["enforceReferences"]) { throw new InitializationError(scope, scope._schema, "{full}", "Unknown schema reference", uri); } else { uri = null; } } return func.apply(scope, arguments); }; } /** * Wraps all JSONInstance methods with a function that resolves the "full" reference. * * @inner */ (function () { var key; for (key in JSONSchema.prototype) { if (JSONSchema.prototype[key] !== O[key] && typeOf(JSONSchema.prototype[key]) === "function") { JSONSchema.prototype[key] = createFullLookupWrapper(JSONSchema.prototype[key]); } } }()); /** * Reinitializes/re-registers/rebuilds the schema. *
* This is used internally, and should only be called when a schema's private variables are modified directly. * * @private * @return {JSONSchema} The newly rebuilt schema */ JSONSchema.prototype.rebuild = function () { var instance = this, initializer = instance.getSchema().getValueOfProperty("initializer"); //clear previous built values instance._refs = null; instance._attributes = null; if (typeof initializer === "function") { instance = initializer(instance); } //register schema instance._env._schemas[instance._uri] = instance; //build & cache the rest of the schema instance.getAttributes(); return instance; }; /** * Set the provided reference to the given value. *
* References are used for establishing soft-links to other {@link JSONSchema}s. * Currently, the following references are natively supported: *
*
full
*
The value is the URI to the full instance of this instance.
*
describedby
*
The value is the URI to the schema of this instance.
*
* * @param {String} name The name of the reference * @param {String} uri The URI of the schema to refer to */ JSONSchema.prototype.setReference = function (name, uri) { if (!this._refs) { this._refs = {}; } this._refs[name] = this.resolveURI(uri); }; /** * Returns the value of the provided reference name. * * @param {String} name The name of the reference * @return {String} The value of the provided reference name */ JSONSchema.prototype.getReference = function (name) { return this._refs && this._refs[name]; }; /** * Merges two schemas/instances together. */ function inherits(base, extra, extension) { var baseType = typeOf(base), extraType = typeOf(extra), child, x; if (extraType === "undefined") { return clone(base, true); } else if (baseType === "undefined" || extraType !== baseType) { return clone(extra, true); } else if (extraType === "object") { if (base instanceof JSONSchema) { base = base.getAttributes(); } if (extra instanceof JSONSchema) { extra = extra.getAttributes(); if (extra["extends"] && extension && extra["extends"] instanceof JSONSchema) { extra["extends"] = [ extra["extends"] ]; } } child = clone(base, true); //this could be optimized as some properties get overwritten for (x in extra) { if (extra[x] !== O[x]) { child[x] = inherits(base[x], extra[x], extension); } } return child; } else { return clone(extra, true); } } /** * An Environment is a sandbox of schemas thats behavior is different from other environments. * * @name Environment * @class */ function Environment() { this._id = randomUUID(); this._schemas = {}; this._options = {}; this.createSchema({}, true, "urn:jsv:empty-schema#"); } /** * Returns a clone of the target environment. * * @returns {Environment} A new {@link Environment} that is a exact copy of the target environment */ Environment.prototype.clone = function () { var env = new Environment(); env._schemas = createObject(this._schemas); env._options = createObject(this._options); return env; }; /** * Returns a new {@link JSONInstance} of the provided data. * * @param {JSONInstance|Any} data The value of the instance * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. * @returns {JSONInstance} A new {@link JSONInstance} from the provided data */ Environment.prototype.createInstance = function (data, uri) { uri = formatURI(uri); if (data instanceof JSONInstance && (!uri || data.getURI() === uri)) { return data; } return new JSONInstance(this, data, uri); }; /** * Creates a new {@link JSONSchema} from the provided data, and registers it with the environment. * * @param {JSONInstance|Any} data The value of the schema * @param {JSONSchema|Boolean} [schema] The schema to bind to the instance. If undefined, the environment's default schema will be used. If true, the instance's schema will be itself. * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. * @returns {JSONSchema} A new {@link JSONSchema} from the provided data * @throws {InitializationError} If a schema that is not registered with the environment is referenced */ Environment.prototype.createSchema = function (data, schema, uri) { uri = formatURI(uri); if (data instanceof JSONSchema && (!uri || data._uri === uri) && (!schema || data.getSchema().equals(schema))) { return data; } return new JSONSchema(this, data, uri, schema); }; /** * Creates an empty schema. * * @returns {JSONSchema} The empty schema, who's schema is itself. */ Environment.prototype.createEmptySchema = function () { return this._schemas["urn:jsv:empty-schema#"]; }; /** * Returns the schema registered with the provided URI. * * @param {String} uri The absolute URI of the required schema * @returns {JSONSchema|undefined} The request schema, or undefined if not found */ Environment.prototype.findSchema = function (uri) { return this._schemas[formatURI(uri)]; }; /** * Sets the specified environment option to the specified value. * * @param {String} name The name of the environment option to set * @param {Any} value The new value of the environment option */ Environment.prototype.setOption = function (name, value) { this._options[name] = value; }; /** * Returns the specified environment option. * * @param {String} name The name of the environment option to set * @returns {Any} The value of the environment option */ Environment.prototype.getOption = function (name) { return this._options[name]; }; /** * Sets the default fragment delimiter of the environment. * * @deprecated Use {@link Environment#setOption} with option "defaultFragmentDelimiter" * @param {String} fd The fragment delimiter character */ Environment.prototype.setDefaultFragmentDelimiter = function (fd) { if (typeof fd === "string" && fd.length > 0) { this._options["defaultFragmentDelimiter"] = fd; } }; /** * Returns the default fragment delimiter of the environment. * * @deprecated Use {@link Environment#getOption} with option "defaultFragmentDelimiter" * @returns {String} The fragment delimiter character */ Environment.prototype.getDefaultFragmentDelimiter = function () { return this._options["defaultFragmentDelimiter"]; }; /** * Sets the URI of the default schema for the environment. * * @deprecated Use {@link Environment#setOption} with option "defaultSchemaURI" * @param {String} uri The default schema URI */ Environment.prototype.setDefaultSchemaURI = function (uri) { if (typeof uri === "string") { this._options["defaultSchemaURI"] = formatURI(uri); } }; /** * Returns the default schema of the environment. * * @returns {JSONSchema} The default schema */ Environment.prototype.getDefaultSchema = function () { return this.findSchema(this._options["defaultSchemaURI"]); }; /** * Validates both the provided schema and the provided instance, and returns a {@link Report}. * If the schema fails to validate, the instance will not be validated. * * @param {JSONInstance|Any} instanceJSON The {@link JSONInstance} or JavaScript value to validate. * @param {JSONSchema|Any} schemaJSON The {@link JSONSchema} or JavaScript value to use in the validation. This will also be validated againt the schema's schema. * @returns {Report} The result of the validation */ Environment.prototype.validate = function (instanceJSON, schemaJSON) { var instance, schema, schemaSchema, report = new Report(); try { instance = this.createInstance(instanceJSON); report.instance = instance; } catch (e) { report.addError(e.uri, e.schemaUri, e.attribute, e.message, e.details); } try { schema = this.createSchema(schemaJSON); report.schema = schema; schemaSchema = schema.getSchema(); report.schemaSchema = schemaSchema; } catch (f) { report.addError(f.uri, f.schemaUri, f.attribute, f.message, f.details); } if (schemaSchema) { schemaSchema.validate(schema, report); } if (report.errors.length) { return report; } return schema.validate(instance, report); }; /** * @private */ Environment.prototype._checkForInvalidInstances = function (stackSize, schemaURI) { var result = [], stack = [ [schemaURI, this._schemas[schemaURI]] ], counter = 0, item, uri, instance, properties, key; while (counter++ < stackSize && stack.length) { item = stack.shift(); uri = item[0]; instance = item[1]; if (instance instanceof JSONSchema) { if (this._schemas[instance._uri] !== instance) { result.push("Instance " + uri + " does not match " + instance._uri); } else { //schema = instance.getSchema(); //stack.push([uri + "/{schema}", schema]); properties = instance.getAttributes(); for (key in properties) { if (properties[key] !== O[key]) { stack.push([uri + "/" + escapeURIComponent(key), properties[key]]); } } } } else if (typeOf(instance) === "object") { properties = instance; for (key in properties) { if (properties.hasOwnProperty(key)) { stack.push([uri + "/" + escapeURIComponent(key), properties[key]]); } } } else if (typeOf(instance) === "array") { properties = instance; for (key = 0; key < properties.length; ++key) { stack.push([uri + "/" + escapeURIComponent(key), properties[key]]); } } } return result.length ? result : counter; }; /** * A globaly accessible object that provides the ability to create and manage {@link Environments}, * as well as providing utility methods. * * @namespace */ JSV = { _environments : {}, _defaultEnvironmentID : "", /** * Returns if the provide value is an instance of {@link JSONInstance}. * * @param o The value to test * @returns {Boolean} If the provide value is an instance of {@link JSONInstance} */ isJSONInstance : function (o) { return o instanceof JSONInstance; }, /** * Returns if the provide value is an instance of {@link JSONSchema}. * * @param o The value to test * @returns {Boolean} If the provide value is an instance of {@link JSONSchema} */ isJSONSchema : function (o) { return o instanceof JSONSchema; }, /** * Creates and returns a new {@link Environment} that is a clone of the environment registered with the provided ID. * If no environment ID is provided, the default environment is cloned. * * @param {String} [id] The ID of the environment to clone. If undefined, the default environment ID is used. * @returns {Environment} A newly cloned {@link Environment} * @throws {Error} If there is no environment registered with the provided ID */ createEnvironment : function (id) { id = id || this._defaultEnvironmentID; if (!this._environments[id]) { throw new Error("Unknown Environment ID"); } //else return this._environments[id].clone(); }, Environment : Environment, /** * Registers the provided {@link Environment} with the provided ID. * * @param {String} id The ID of the environment * @param {Environment} env The environment to register */ registerEnvironment : function (id, env) { id = id || (env || 0)._id; if (id && !this._environments[id] && env instanceof Environment) { env._id = id; this._environments[id] = env; } }, /** * Sets which registered ID is the default environment. * * @param {String} id The ID of the registered environment that is default * @throws {Error} If there is no registered environment with the provided ID */ setDefaultEnvironmentID : function (id) { if (typeof id === "string") { if (!this._environments[id]) { throw new Error("Unknown Environment ID"); } this._defaultEnvironmentID = id; } }, /** * Returns the ID of the default environment. * * @returns {String} The ID of the default environment */ getDefaultEnvironmentID : function () { return this._defaultEnvironmentID; }, // // Utility Functions // /** * Returns the name of the type of the provided value. * * @event //utility * @param {Any} o The value to determine the type of * @returns {String} The name of the type of the value */ typeOf : typeOf, /** * Return a new object that inherits all of the properties of the provided object. * * @event //utility * @param {Object} proto The prototype of the new object * @returns {Object} A new object that inherits all of the properties of the provided object */ createObject : createObject, /** * Returns a new object with each property transformed by the iterator. * * @event //utility * @param {Object} obj The object to transform * @param {Function} iterator A function that returns the new value of the provided property * @param {Object} [scope] The value of this in the iterator * @returns {Object} A new object with each property transformed */ mapObject : mapObject, /** * Returns a new array with each item transformed by the iterator. * * @event //utility * @param {Array} arr The array to transform * @param {Function} iterator A function that returns the new value of the provided item * @param {Object} scope The value of this in the iterator * @returns {Array} A new array with each item transformed */ mapArray : mapArray, /** * Returns a new array that only contains the items allowed by the iterator. * * @event //utility * @param {Array} arr The array to filter * @param {Function} iterator The function that returns true if the provided property should be added to the array * @param {Object} scope The value of this within the iterator * @returns {Array} A new array that contains the items allowed by the iterator */ filterArray : filterArray, /** * Returns the first index in the array that the provided item is located at. * * @event //utility * @param {Array} arr The array to search * @param {Any} o The item being searched for * @returns {Number} The index of the item in the array, or -1 if not found */ searchArray : searchArray, /** * Returns an array representation of a value. * * * @event //utility * @param {Any} o The value to convert into an array * @returns {Array} The value as an array */ toArray : toArray, /** * Returns an array of the names of all properties of an object. * * @event //utility * @param {Object|Array} o The object in question * @returns {Array} The names of all properties */ keys : keys, /** * Mutates the array by pushing the provided value onto the array only if it is not already there. * * @event //utility * @param {Array} arr The array to modify * @param {Any} o The object to add to the array if it is not already there * @returns {Array} The provided array for chaining */ pushUnique : pushUnique, /** * Mutates the array by removing the first item that matches the provided value in the array. * * @event //utility * @param {Array} arr The array to modify * @param {Any} o The object to remove from the array * @returns {Array} The provided array for chaining */ popFirst : popFirst, /** * Creates a copy of the target object. *

* This method will create a new instance of the target, and then mixin the properties of the target. * If deep is true, then each property will be cloned before mixin. *

*

Warning: This is not a generic clone function, as it will only properly clone objects and arrays.

* * @event //utility * @param {Any} o The value to clone * @param {Boolean} [deep=false] If each property should be recursively cloned * @returns A cloned copy of the provided value */ clone : clone, /** * Generates a pseudo-random UUID. * * @event //utility * @returns {String} A new universally unique ID */ randomUUID : randomUUID, /** * Properly escapes a URI component for embedding into a URI string. * * @event //utility * @param {String} str The URI component to escape * @returns {String} The escaped URI component */ escapeURIComponent : escapeURIComponent, /** * Returns a URI that is formated for JSV. Currently, this only ensures that the URI ends with a hash tag (#). * * @event //utility * @param {String} uri The URI to format * @returns {String} The URI formatted for JSV */ formatURI : formatURI, /** * Merges two schemas/instance together. * * @event //utility * @param {JSONSchema|Any} base The old value to merge * @param {JSONSchema|Any} extra The new value to merge * @param {Boolean} extension If the merge is a JSON Schema extension * @return {Any} The modified base value */ inherits : inherits, /** * @private * @event //utility */ InitializationError : InitializationError }; this.JSV = JSV; //set global object exports.JSV = JSV; //export to CommonJS require("./environments"); //load default environments }());