1 /** 2 * JSV: JSON Schema Validator 3 * 4 * @fileOverview A JavaScript implementation of a extendable, fully compliant JSON Schema validator. 5 * @author <a href="mailto:gary.court@gmail.com">Gary Court</a> 6 * @version 4.0 7 * @see http://github.com/garycourt/JSV 8 */ 9 10 /* 11 * Copyright 2010 Gary Court. All rights reserved. 12 * 13 * Redistribution and use in source and binary forms, with or without modification, are 14 * permitted provided that the following conditions are met: 15 * 16 * 1. Redistributions of source code must retain the above copyright notice, this list of 17 * conditions and the following disclaimer. 18 * 19 * 2. Redistributions in binary form must reproduce the above copyright notice, this list 20 * of conditions and the following disclaimer in the documentation and/or other materials 21 * provided with the distribution. 22 * 23 * THIS SOFTWARE IS PROVIDED BY GARY COURT ``AS IS'' AND ANY EXPRESS OR IMPLIED 24 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 25 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR 26 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 27 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 31 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 * 33 * The views and conclusions contained in the software and documentation are those of the 34 * authors and should not be interpreted as representing official policies, either expressed 35 * or implied, of Gary Court or the JSON Schema specification. 36 */ 37 38 /*jslint white: true, sub: true, onevar: true, undef: true, eqeqeq: true, newcap: true, immed: true, indent: 4 */ 39 40 var exports = exports || this, 41 require = require || function () { 42 return exports; 43 }; 44 45 (function () { 46 47 var URI = require("./uri/uri").URI, 48 O = {}, 49 I2H = "0123456789abcdef".split(""), 50 mapArray, filterArray, searchArray, 51 52 JSV; 53 54 // 55 // Utility functions 56 // 57 58 function typeOf(o) { 59 return o === undefined ? "undefined" : (o === null ? "null" : Object.prototype.toString.call(o).split(" ").pop().split("]").shift().toLowerCase()); 60 } 61 62 /** @inner */ 63 function F() {} 64 65 function createObject(proto) { 66 F.prototype = proto || {}; 67 return new F(); 68 } 69 70 function mapObject(obj, func, scope) { 71 var newObj = {}, key; 72 for (key in obj) { 73 if (obj[key] !== O[key]) { 74 newObj[key] = func.call(scope, obj[key], key, obj); 75 } 76 } 77 return newObj; 78 } 79 80 /** @ignore */ 81 mapArray = function (arr, func, scope) { 82 var x = 0, xl = arr.length, newArr = new Array(xl); 83 for (; x < xl; ++x) { 84 newArr[x] = func.call(scope, arr[x], x, arr); 85 } 86 return newArr; 87 }; 88 89 if (Array.prototype.map) { 90 /** @ignore */ 91 mapArray = function (arr, func, scope) { 92 return Array.prototype.map.call(arr, func, scope); 93 }; 94 } 95 96 /** @ignore */ 97 filterArray = function (arr, func, scope) { 98 var x = 0, xl = arr.length, newArr = []; 99 for (; x < xl; ++x) { 100 if (func.call(scope, arr[x], x, arr)) { 101 newArr[newArr.length] = arr[x]; 102 } 103 } 104 return newArr; 105 }; 106 107 if (Array.prototype.filter) { 108 /** @ignore */ 109 filterArray = function (arr, func, scope) { 110 return Array.prototype.filter.call(arr, func, scope); 111 }; 112 } 113 114 /** @ignore */ 115 searchArray = function (arr, o) { 116 var x = 0, xl = arr.length; 117 for (; x < xl; ++x) { 118 if (arr[x] === o) { 119 return x; 120 } 121 } 122 return -1; 123 }; 124 125 if (Array.prototype.indexOf) { 126 /** @ignore */ 127 searchArray = function (arr, o) { 128 return Array.prototype.indexOf.call(arr, o); 129 }; 130 } 131 132 function toArray(o) { 133 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))) : []; 134 } 135 136 function keys(o) { 137 var result = [], key; 138 139 switch (typeOf(o)) { 140 case "object": 141 for (key in o) { 142 if (o[key] !== O[key]) { 143 result[result.length] = key; 144 } 145 } 146 break; 147 case "array": 148 for (key = o.length - 1; key >= 0; --key) { 149 result[key] = key; 150 } 151 break; 152 } 153 154 return result; 155 } 156 157 function pushUnique(arr, o) { 158 if (searchArray(arr, o) === -1) { 159 arr.push(o); 160 } 161 return arr; 162 } 163 164 function popFirst(arr, o) { 165 var index = searchArray(arr, o); 166 if (index > -1) { 167 arr.splice(index, 1); 168 } 169 return arr; 170 } 171 172 function randomUUID() { 173 return [ 174 I2H[Math.floor(Math.random() * 0x10)], 175 I2H[Math.floor(Math.random() * 0x10)], 176 I2H[Math.floor(Math.random() * 0x10)], 177 I2H[Math.floor(Math.random() * 0x10)], 178 I2H[Math.floor(Math.random() * 0x10)], 179 I2H[Math.floor(Math.random() * 0x10)], 180 I2H[Math.floor(Math.random() * 0x10)], 181 I2H[Math.floor(Math.random() * 0x10)], 182 "-", 183 I2H[Math.floor(Math.random() * 0x10)], 184 I2H[Math.floor(Math.random() * 0x10)], 185 I2H[Math.floor(Math.random() * 0x10)], 186 I2H[Math.floor(Math.random() * 0x10)], 187 "-4", //set 4 high bits of time_high field to version 188 I2H[Math.floor(Math.random() * 0x10)], 189 I2H[Math.floor(Math.random() * 0x10)], 190 I2H[Math.floor(Math.random() * 0x10)], 191 "-", 192 I2H[(Math.floor(Math.random() * 0x10) & 0x3) | 0x8], //specify 2 high bits of clock sequence 193 I2H[Math.floor(Math.random() * 0x10)], 194 I2H[Math.floor(Math.random() * 0x10)], 195 I2H[Math.floor(Math.random() * 0x10)], 196 "-", 197 I2H[Math.floor(Math.random() * 0x10)], 198 I2H[Math.floor(Math.random() * 0x10)], 199 I2H[Math.floor(Math.random() * 0x10)], 200 I2H[Math.floor(Math.random() * 0x10)], 201 I2H[Math.floor(Math.random() * 0x10)], 202 I2H[Math.floor(Math.random() * 0x10)], 203 I2H[Math.floor(Math.random() * 0x10)], 204 I2H[Math.floor(Math.random() * 0x10)], 205 I2H[Math.floor(Math.random() * 0x10)], 206 I2H[Math.floor(Math.random() * 0x10)], 207 I2H[Math.floor(Math.random() * 0x10)], 208 I2H[Math.floor(Math.random() * 0x10)] 209 ].join(""); 210 } 211 212 function escapeURIComponent(str) { 213 return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A'); 214 } 215 216 function formatURI(uri) { 217 if (typeof uri === "string" && uri.indexOf("#") === -1) { 218 uri += "#"; 219 } 220 return uri; 221 } 222 223 function stripInstances(o) { 224 if (o instanceof JSONInstance) { 225 return o.getURI(); 226 } 227 228 switch (typeOf(o)) { 229 case "undefined": 230 case "null": 231 case "boolean": 232 case "number": 233 case "string": 234 return o; //do nothing 235 236 case "object": 237 return mapObject(o, stripInstances); 238 239 case "array": 240 return mapArray(o, stripInstances); 241 242 default: 243 return o.toString(); 244 } 245 } 246 247 /** 248 * The exception that is thrown when a schema fails to be created. 249 * 250 * @name InitializationError 251 * @class 252 * @param {JSONInstance|String} instance The instance (or instance URI) that is invalid 253 * @param {JSONSchema|String} schema The schema (or schema URI) that was validating the instance 254 * @param {String} attr The attribute that failed to validated 255 * @param {String} message A user-friendly message on why the schema attribute failed to validate the instance 256 * @param {Any} details The value of the schema attribute 257 */ 258 259 function InitializationError(instance, schema, attr, message, details) { 260 Error.call(this, message); 261 262 this.uri = instance instanceof JSONInstance ? instance.getURI() : instance; 263 this.schemaUri = schema instanceof JSONInstance ? schema.getURI() : schema; 264 this.attribute = attr; 265 this.message = message; 266 this.description = message; //IE 267 this.details = details; 268 } 269 270 InitializationError.prototype = new Error(); 271 InitializationError.prototype.constructor = InitializationError; 272 InitializationError.prototype.name = "InitializationError"; 273 274 /** 275 * Defines an error, found by a schema, with an instance. 276 * This class can only be instantiated by {@link Report#addError}. 277 * 278 * @name ValidationError 279 * @class 280 * @see Report#addError 281 */ 282 283 /** 284 * The URI of the instance that has the error. 285 * 286 * @name ValidationError.prototype.uri 287 * @type String 288 */ 289 290 /** 291 * The URI of the schema that generated the error. 292 * 293 * @name ValidationError.prototype.schemaUri 294 * @type String 295 */ 296 297 /** 298 * The name of the schema attribute that generated the error. 299 * 300 * @name ValidationError.prototype.attribute 301 * @type String 302 */ 303 304 /** 305 * An user-friendly (English) message about what failed to validate. 306 * 307 * @name ValidationError.prototype.message 308 * @type String 309 */ 310 311 /** 312 * The value of the schema attribute that generated the error. 313 * 314 * @name ValidationError.prototype.details 315 * @type Any 316 */ 317 318 /** 319 * Reports are returned from validation methods to describe the result of a validation. 320 * 321 * @name Report 322 * @class 323 * @see JSONSchema#validate 324 * @see Environment#validate 325 */ 326 327 function Report() { 328 /** 329 * An array of {@link ValidationError} objects that define all the errors generated by the schema against the instance. 330 * 331 * @name Report.prototype.errors 332 * @type Array 333 * @see Report#addError 334 */ 335 this.errors = []; 336 337 /** 338 * A hash table of every instance and what schemas were validated against it. 339 * <p> 340 * The key of each item in the table is the URI of the instance that was validated. 341 * The value of this key is an array of strings of URIs of the schema that validated it. 342 * </p> 343 * 344 * @name Report.prototype.validated 345 * @type Object 346 * @see Report#registerValidation 347 * @see Report#isValidatedBy 348 */ 349 this.validated = {}; 350 351 /** 352 * If the report is generated by {@link Environment#validate}, this field is the generated instance. 353 * 354 * @name Report.prototype.instance 355 * @type JSONInstance 356 * @see Environment#validate 357 */ 358 359 /** 360 * If the report is generated by {@link Environment#validate}, this field is the generated schema. 361 * 362 * @name Report.prototype.schema 363 * @type JSONSchema 364 * @see Environment#validate 365 */ 366 367 /** 368 * If the report is generated by {@link Environment#validate}, this field is the schema's schema. 369 * This value is the same as calling <code>schema.getSchema()</code>. 370 * 371 * @name Report.prototype.schemaSchema 372 * @type JSONSchema 373 * @see Environment#validate 374 * @see JSONSchema#getSchema 375 */ 376 } 377 378 /** 379 * Adds a {@link ValidationError} object to the <a href="#errors"><code>errors</code></a> field. 380 * 381 * @param {JSONInstance|String} instance The instance (or instance URI) that is invalid 382 * @param {JSONSchema|String} schema The schema (or schema URI) that was validating the instance 383 * @param {String} attr The attribute that failed to validated 384 * @param {String} message A user-friendly message on why the schema attribute failed to validate the instance 385 * @param {Any} details The value of the schema attribute 386 */ 387 388 Report.prototype.addError = function (instance, schema, attr, message, details) { 389 this.errors.push({ 390 uri : instance instanceof JSONInstance ? instance.getURI() : instance, 391 schemaUri : schema instanceof JSONInstance ? schema.getURI() : schema, 392 attribute : attr, 393 message : message, 394 details : stripInstances(details) 395 }); 396 }; 397 398 /** 399 * Registers that the provided instance URI has been validated by the provided schema URI. 400 * This is recorded in the <a href="#validated"><code>validated</code></a> field. 401 * 402 * @param {String} uri The URI of the instance that was validated 403 * @param {String} schemaUri The URI of the schema that validated the instance 404 */ 405 406 Report.prototype.registerValidation = function (uri, schemaUri) { 407 if (!this.validated[uri]) { 408 this.validated[uri] = [ schemaUri ]; 409 } else { 410 this.validated[uri].push(schemaUri); 411 } 412 }; 413 414 /** 415 * Returns if an instance with the provided URI has been validated by the schema with the provided URI. 416 * 417 * @param {String} uri The URI of the instance 418 * @param {String} schemaUri The URI of a schema 419 * @returns {Boolean} If the instance has been validated by the schema. 420 */ 421 422 Report.prototype.isValidatedBy = function (uri, schemaUri) { 423 return !!this.validated[uri] && searchArray(this.validated[uri], schemaUri) !== -1; 424 }; 425 426 /** 427 * A wrapper class for binding an Environment, URI and helper methods to an instance. 428 * This class is most commonly instantiated with {@link Environment#createInstance}. 429 * 430 * @name JSONInstance 431 * @class 432 * @param {Environment} env The environment this instance belongs to 433 * @param {JSONInstance|Any} json The value of the instance 434 * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. 435 * @param {String} [fd] The fragment delimiter for properties. If undefined, uses the environment default. 436 */ 437 438 function JSONInstance(env, json, uri, fd) { 439 if (json instanceof JSONInstance) { 440 if (typeof fd !== "string") { 441 fd = json._fd; 442 } 443 if (typeof uri !== "string") { 444 uri = json._uri; 445 } 446 json = json._value; 447 } 448 449 if (typeof uri !== "string") { 450 uri = "urn:uuid:" + randomUUID() + "#"; 451 } else if (uri.indexOf(":") === -1) { 452 uri = formatURI(URI.resolve("urn:uuid:" + randomUUID() + "#", uri)); 453 } 454 455 this._env = env; 456 this._value = json; 457 this._uri = uri; 458 this._fd = fd || this._env._options["defaultFragmentDelimiter"]; 459 } 460 461 /** 462 * Returns the environment the instance is bound to. 463 * 464 * @returns {Environment} The environment of the instance 465 */ 466 467 JSONInstance.prototype.getEnvironment = function () { 468 return this._env; 469 }; 470 471 /** 472 * Returns the name of the type of the instance. 473 * 474 * @returns {String} The name of the type of the instance 475 */ 476 477 JSONInstance.prototype.getType = function () { 478 return typeOf(this._value); 479 }; 480 481 /** 482 * Returns the JSON value of the instance. 483 * 484 * @returns {Any} The actual JavaScript value of the instance 485 */ 486 487 JSONInstance.prototype.getValue = function () { 488 return this._value; 489 }; 490 491 /** 492 * Returns the URI of the instance. 493 * 494 * @returns {String} The URI of the instance 495 */ 496 497 JSONInstance.prototype.getURI = function () { 498 return this._uri; 499 }; 500 501 /** 502 * Returns a resolved URI of a provided relative URI against the URI of the instance. 503 * 504 * @param {String} uri The relative URI to resolve 505 * @returns {String} The resolved URI 506 */ 507 508 JSONInstance.prototype.resolveURI = function (uri) { 509 return formatURI(URI.resolve(this._uri, uri)); 510 }; 511 512 /** 513 * Returns an array of the names of all the properties. 514 * 515 * @returns {Array} An array of strings which are the names of all the properties 516 */ 517 518 JSONInstance.prototype.getPropertyNames = function () { 519 return keys(this._value); 520 }; 521 522 /** 523 * Returns a {@link JSONInstance} of the value of the provided property name. 524 * 525 * @param {String} key The name of the property to fetch 526 * @returns {JSONInstance} The instance of the property value 527 */ 528 529 JSONInstance.prototype.getProperty = function (key) { 530 var value = this._value ? this._value[key] : undefined; 531 if (value instanceof JSONInstance) { 532 return value; 533 } 534 //else 535 return new JSONInstance(this._env, value, this._uri + this._fd + escapeURIComponent(key), this._fd); 536 }; 537 538 /** 539 * Returns all the property instances of the target instance. 540 * <p> 541 * If the target instance is an Object, then the method will return a hash table of {@link JSONInstance}s of all the properties. 542 * If the target instance is an Array, then the method will return an array of {@link JSONInstance}s of all the items. 543 * </p> 544 * 545 * @returns {Object|Array|undefined} The list of instances for all the properties 546 */ 547 548 JSONInstance.prototype.getProperties = function () { 549 var type = typeOf(this._value), 550 self = this; 551 552 if (type === "object") { 553 return mapObject(this._value, function (value, key) { 554 if (value instanceof JSONInstance) { 555 return value; 556 } 557 return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd); 558 }); 559 } else if (type === "array") { 560 return mapArray(this._value, function (value, key) { 561 if (value instanceof JSONInstance) { 562 return value; 563 } 564 return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd); 565 }); 566 } 567 }; 568 569 /** 570 * Returns the JSON value of the provided property name. 571 * This method is a faster version of calling <code>instance.getProperty(key).getValue()</code>. 572 * 573 * @param {String} key The name of the property 574 * @returns {Any} The JavaScript value of the instance 575 * @see JSONInstance#getProperty 576 * @see JSONInstance#getValue 577 */ 578 579 JSONInstance.prototype.getValueOfProperty = function (key) { 580 if (this._value) { 581 if (this._value[key] instanceof JSONInstance) { 582 return this._value[key]._value; 583 } 584 return this._value[key]; 585 } 586 }; 587 588 /** 589 * Return if the provided value is the same as the value of the instance. 590 * 591 * @param {JSONInstance|Any} instance The value to compare 592 * @returns {Boolean} If both the instance and the value match 593 */ 594 595 JSONInstance.prototype.equals = function (instance) { 596 if (instance instanceof JSONInstance) { 597 return this._value === instance._value; 598 } 599 //else 600 return this._value === instance; 601 }; 602 603 /** 604 * Warning: Not a generic clone function 605 * Produces a JSV acceptable clone 606 */ 607 608 function clone(obj, deep) { 609 var newObj, x; 610 611 if (obj instanceof JSONInstance) { 612 obj = obj.getValue(); 613 } 614 615 switch (typeOf(obj)) { 616 case "object": 617 if (deep) { 618 newObj = {}; 619 for (x in obj) { 620 if (obj[x] !== O[x]) { 621 newObj[x] = clone(obj[x], deep); 622 } 623 } 624 return newObj; 625 } else { 626 return createObject(obj); 627 } 628 break; 629 case "array": 630 if (deep) { 631 newObj = new Array(obj.length); 632 x = obj.length; 633 while (--x >= 0) { 634 newObj[x] = clone(obj[x], deep); 635 } 636 return newObj; 637 } else { 638 return Array.prototype.slice.call(obj); 639 } 640 break; 641 default: 642 return obj; 643 } 644 } 645 646 /** 647 * This class binds a {@link JSONInstance} with a {@link JSONSchema} to provided context aware methods. 648 * 649 * @name JSONSchema 650 * @class 651 * @param {Environment} env The environment this schema belongs to 652 * @param {JSONInstance|Any} json The value of the schema 653 * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. 654 * @param {JSONSchema|Boolean} [schema] The schema to bind to the instance. If <code>undefined</code>, the environment's default schema will be used. If <code>true</code>, the instance's schema will be itself. 655 * @extends JSONInstance 656 */ 657 658 function JSONSchema(env, json, uri, schema) { 659 var fr; 660 JSONInstance.call(this, env, json, uri); 661 662 if (schema === true) { 663 this._schema = this; 664 } else if (json instanceof JSONSchema && !(schema instanceof JSONSchema)) { 665 this._schema = json._schema; //TODO: Make sure cross environments don't mess everything up 666 } else { 667 this._schema = schema instanceof JSONSchema ? schema : this._env.getDefaultSchema() || this._env.createEmptySchema(); 668 } 669 670 //determine fragment delimiter from schema 671 fr = this._schema.getValueOfProperty("fragmentResolution"); 672 if (fr === "dot-delimited") { 673 this._fd = "."; 674 } else if (fr === "slash-delimited") { 675 this._fd = "/"; 676 } 677 678 return this.rebuild(); //this works even when called with "new" 679 } 680 681 JSONSchema.prototype = createObject(JSONInstance.prototype); 682 683 /** 684 * Returns the schema of the schema. 685 * 686 * @returns {JSONSchema} The schema of the schema 687 */ 688 689 JSONSchema.prototype.getSchema = function () { 690 var uri = this._refs && this._refs["describedby"], 691 newSchema; 692 693 if (uri) { 694 newSchema = uri && this._env.findSchema(uri); 695 696 if (newSchema) { 697 if (!newSchema.equals(this._schema)) { 698 this._schema = newSchema; 699 this.rebuild(); //if the schema has changed, the context has changed - so everything must be rebuilt 700 } 701 } else if (this._env._options["enforceReferences"]) { 702 throw new InitializationError(this, this._schema, "{describedby}", "Unknown schema reference", uri); 703 } 704 } 705 706 return this._schema; 707 }; 708 709 /** 710 * Returns the value of the provided attribute name. 711 * <p> 712 * This method is different from {@link JSONInstance#getProperty} as the named property 713 * is converted using a parser defined by the schema's schema before being returned. This 714 * makes the return value of this method attribute dependent. 715 * </p> 716 * 717 * @param {String} key The name of the attribute 718 * @param {Any} [arg] Some attribute parsers accept special arguments for returning resolved values. This is attribute dependent. 719 * @returns {JSONSchema|Any} The value of the attribute 720 */ 721 722 JSONSchema.prototype.getAttribute = function (key, arg) { 723 var schemaProperty, parser, property, result, 724 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 725 726 if (!arg && this._attributes && this._attributes.hasOwnProperty(key)) { 727 return this._attributes[key]; 728 } 729 730 schemaProperty = schema.getProperty("properties").getProperty(key); 731 parser = schemaProperty.getValueOfProperty("parser"); 732 property = this.getProperty(key); 733 if (typeof parser === "function") { 734 result = parser(property, schemaProperty, arg); 735 if (!arg && this._attributes) { 736 this._attributes[key] = result; 737 } 738 return result; 739 } 740 //else 741 return property.getValue(); 742 }; 743 744 /** 745 * Returns all the attributes of the schema. 746 * 747 * @returns {Object} A map of all parsed attribute values 748 */ 749 750 JSONSchema.prototype.getAttributes = function () { 751 var properties, schemaProperties, key, schemaProperty, parser, 752 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 753 754 if (!this._attributes && this.getType() === "object") { 755 properties = this.getProperties(); 756 schemaProperties = schema.getProperty("properties"); 757 this._attributes = {}; 758 for (key in properties) { 759 if (properties[key] !== O[key]) { 760 schemaProperty = schemaProperties && schemaProperties.getProperty(key); 761 parser = schemaProperty && schemaProperty.getValueOfProperty("parser"); 762 if (typeof parser === "function") { 763 this._attributes[key] = parser(properties[key], schemaProperty); 764 } else { 765 this._attributes[key] = properties[key].getValue(); 766 } 767 } 768 } 769 } 770 771 return clone(this._attributes, false); 772 }; 773 774 /** 775 * Convenience method for retrieving a link or link object from a schema. 776 * This method is the same as calling <code>schema.getAttribute("links", [rel, instance])[0];</code>. 777 * 778 * @param {String} rel The link relationship 779 * @param {JSONInstance} [instance] The instance to resolve any URIs from 780 * @returns {String|Object|undefined} If <code>instance</code> is provided, a string containing the resolve URI of the link is returned. 781 * If <code>instance</code> is not provided, a link object is returned with details of the link. 782 * If no link with the provided relationship exists, <code>undefined</code> is returned. 783 * @see JSONSchema#getAttribute 784 */ 785 786 JSONSchema.prototype.getLink = function (rel, instance) { 787 var schemaLinks = this.getAttribute("links", [rel, instance]); 788 if (schemaLinks && schemaLinks.length && schemaLinks[schemaLinks.length - 1]) { 789 return schemaLinks[schemaLinks.length - 1]; 790 } 791 }; 792 793 /** 794 * Validates the provided instance against the target schema and returns a {@link Report}. 795 * 796 * @param {JSONInstance|Any} instance The instance to validate; may be a {@link JSONInstance} or any JavaScript value 797 * @param {Report} [report] A {@link Report} to concatenate the result of the validation to. If <code>undefined</code>, a new {@link Report} is created. 798 * @param {JSONInstance} [parent] The parent/containing instance of the provided instance 799 * @param {JSONSchema} [parentSchema] The schema of the parent/containing instance 800 * @param {String} [name] The name of the parent object's property that references the instance 801 * @returns {Report} The result of the validation 802 */ 803 804 JSONSchema.prototype.validate = function (instance, report, parent, parentSchema, name) { 805 var schemaSchema = this.getSchema(), 806 validator = schemaSchema.getValueOfProperty("validator"); 807 808 if (!(instance instanceof JSONInstance)) { 809 instance = this.getEnvironment().createInstance(instance); 810 } 811 812 if (!(report instanceof Report)) { 813 report = new Report(); 814 } 815 816 if (this._env._options["validateReferences"] && this._refs) { 817 if (this._refs["describedby"] && !this._env.findSchema(this._refs["describedby"])) { 818 report.addError(this, this._schema, "{describedby}", "Unknown schema reference", this._refs["describedby"]); 819 } 820 if (this._refs["full"] && !this._env.findSchema(this._refs["full"])) { 821 report.addError(this, this._schema, "{full}", "Unknown schema reference", this._refs["full"]); 822 } 823 } 824 825 if (typeof validator === "function" && !report.isValidatedBy(instance.getURI(), this.getURI())) { 826 report.registerValidation(instance.getURI(), this.getURI()); 827 validator(instance, this, schemaSchema, report, parent, parentSchema, name); 828 } 829 830 return report; 831 }; 832 833 /** @inner */ 834 function createFullLookupWrapper(func) { 835 return /** @inner */ function fullLookupWrapper() { 836 var scope = this, 837 stack = [], 838 uri = scope._refs && scope._refs["full"], 839 schema; 840 841 while (uri) { 842 schema = scope._env.findSchema(uri); 843 if (schema) { 844 if (schema._value === scope._value) { 845 break; 846 } 847 scope = schema; 848 stack.push(uri); 849 uri = scope._refs && scope._refs["full"]; 850 if (stack.indexOf(uri) > -1) { 851 break; //stop infinite loop 852 } 853 } else if (scope._env._options["enforceReferences"]) { 854 throw new InitializationError(scope, scope._schema, "{full}", "Unknown schema reference", uri); 855 } else { 856 uri = null; 857 } 858 } 859 return func.apply(scope, arguments); 860 }; 861 } 862 863 /** 864 * Wraps all JSONInstance methods with a function that resolves the "full" reference. 865 * 866 * @inner 867 */ 868 869 (function () { 870 var key; 871 for (key in JSONSchema.prototype) { 872 if (JSONSchema.prototype[key] !== O[key] && typeOf(JSONSchema.prototype[key]) === "function") { 873 JSONSchema.prototype[key] = createFullLookupWrapper(JSONSchema.prototype[key]); 874 } 875 } 876 }()); 877 878 /** 879 * Reinitializes/re-registers/rebuilds the schema. 880 * <br/> 881 * This is used internally, and should only be called when a schema's private variables are modified directly. 882 * 883 * @private 884 * @return {JSONSchema} The newly rebuilt schema 885 */ 886 887 JSONSchema.prototype.rebuild = function () { 888 var instance = this, 889 initializer = instance.getSchema().getValueOfProperty("initializer"); 890 891 //clear previous built values 892 instance._refs = null; 893 instance._attributes = null; 894 895 if (typeof initializer === "function") { 896 instance = initializer(instance); 897 } 898 899 //register schema 900 instance._env._schemas[instance._uri] = instance; 901 902 //build & cache the rest of the schema 903 instance.getAttributes(); 904 905 return instance; 906 }; 907 908 /** 909 * Set the provided reference to the given value. 910 * <br/> 911 * References are used for establishing soft-links to other {@link JSONSchema}s. 912 * Currently, the following references are natively supported: 913 * <dl> 914 * <dt><code>full</code></dt> 915 * <dd>The value is the URI to the full instance of this instance.</dd> 916 * <dt><code>describedby</code></dt> 917 * <dd>The value is the URI to the schema of this instance.</dd> 918 * </dl> 919 * 920 * @param {String} name The name of the reference 921 * @param {String} uri The URI of the schema to refer to 922 */ 923 924 JSONSchema.prototype.setReference = function (name, uri) { 925 if (!this._refs) { 926 this._refs = {}; 927 } 928 this._refs[name] = this.resolveURI(uri); 929 }; 930 931 /** 932 * Returns the value of the provided reference name. 933 * 934 * @param {String} name The name of the reference 935 * @return {String} The value of the provided reference name 936 */ 937 938 JSONSchema.prototype.getReference = function (name) { 939 return this._refs && this._refs[name]; 940 }; 941 942 /** 943 * Merges two schemas/instances together. 944 */ 945 946 function inherits(base, extra, extension) { 947 var baseType = typeOf(base), 948 extraType = typeOf(extra), 949 child, x; 950 951 if (extraType === "undefined") { 952 return clone(base, true); 953 } else if (baseType === "undefined" || extraType !== baseType) { 954 return clone(extra, true); 955 } else if (extraType === "object") { 956 if (base instanceof JSONSchema) { 957 base = base.getAttributes(); 958 } 959 if (extra instanceof JSONSchema) { 960 extra = extra.getAttributes(); 961 if (extra["extends"] && extension && extra["extends"] instanceof JSONSchema) { 962 extra["extends"] = [ extra["extends"] ]; 963 } 964 } 965 child = clone(base, true); //this could be optimized as some properties get overwritten 966 for (x in extra) { 967 if (extra[x] !== O[x]) { 968 child[x] = inherits(base[x], extra[x], extension); 969 } 970 } 971 return child; 972 } else { 973 return clone(extra, true); 974 } 975 } 976 977 /** 978 * An Environment is a sandbox of schemas thats behavior is different from other environments. 979 * 980 * @name Environment 981 * @class 982 */ 983 984 function Environment() { 985 this._id = randomUUID(); 986 this._schemas = {}; 987 this._options = {}; 988 989 this.createSchema({}, true, "urn:jsv:empty-schema#"); 990 } 991 992 /** 993 * Returns a clone of the target environment. 994 * 995 * @returns {Environment} A new {@link Environment} that is a exact copy of the target environment 996 */ 997 998 Environment.prototype.clone = function () { 999 var env = new Environment(); 1000 env._schemas = createObject(this._schemas); 1001 env._options = createObject(this._options); 1002 1003 return env; 1004 }; 1005 1006 /** 1007 * Returns a new {@link JSONInstance} of the provided data. 1008 * 1009 * @param {JSONInstance|Any} data The value of the instance 1010 * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. 1011 * @returns {JSONInstance} A new {@link JSONInstance} from the provided data 1012 */ 1013 1014 Environment.prototype.createInstance = function (data, uri) { 1015 uri = formatURI(uri); 1016 1017 if (data instanceof JSONInstance && (!uri || data.getURI() === uri)) { 1018 return data; 1019 } 1020 1021 return new JSONInstance(this, data, uri); 1022 }; 1023 1024 /** 1025 * Creates a new {@link JSONSchema} from the provided data, and registers it with the environment. 1026 * 1027 * @param {JSONInstance|Any} data The value of the schema 1028 * @param {JSONSchema|Boolean} [schema] The schema to bind to the instance. If <code>undefined</code>, the environment's default schema will be used. If <code>true</code>, the instance's schema will be itself. 1029 * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. 1030 * @returns {JSONSchema} A new {@link JSONSchema} from the provided data 1031 * @throws {InitializationError} If a schema that is not registered with the environment is referenced 1032 */ 1033 1034 Environment.prototype.createSchema = function (data, schema, uri) { 1035 uri = formatURI(uri); 1036 1037 if (data instanceof JSONSchema && (!uri || data._uri === uri) && (!schema || data.getSchema().equals(schema))) { 1038 return data; 1039 } 1040 1041 return new JSONSchema(this, data, uri, schema); 1042 }; 1043 1044 /** 1045 * Creates an empty schema. 1046 * 1047 * @returns {JSONSchema} The empty schema, who's schema is itself. 1048 */ 1049 1050 Environment.prototype.createEmptySchema = function () { 1051 return this._schemas["urn:jsv:empty-schema#"]; 1052 }; 1053 1054 /** 1055 * Returns the schema registered with the provided URI. 1056 * 1057 * @param {String} uri The absolute URI of the required schema 1058 * @returns {JSONSchema|undefined} The request schema, or <code>undefined</code> if not found 1059 */ 1060 1061 Environment.prototype.findSchema = function (uri) { 1062 return this._schemas[formatURI(uri)]; 1063 }; 1064 1065 /** 1066 * Sets the specified environment option to the specified value. 1067 * 1068 * @param {String} name The name of the environment option to set 1069 * @param {Any} value The new value of the environment option 1070 */ 1071 1072 Environment.prototype.setOption = function (name, value) { 1073 this._options[name] = value; 1074 }; 1075 1076 /** 1077 * Returns the specified environment option. 1078 * 1079 * @param {String} name The name of the environment option to set 1080 * @returns {Any} The value of the environment option 1081 */ 1082 1083 Environment.prototype.getOption = function (name) { 1084 return this._options[name]; 1085 }; 1086 1087 /** 1088 * Sets the default fragment delimiter of the environment. 1089 * 1090 * @deprecated Use {@link Environment#setOption} with option "defaultFragmentDelimiter" 1091 * @param {String} fd The fragment delimiter character 1092 */ 1093 1094 Environment.prototype.setDefaultFragmentDelimiter = function (fd) { 1095 if (typeof fd === "string" && fd.length > 0) { 1096 this._options["defaultFragmentDelimiter"] = fd; 1097 } 1098 }; 1099 1100 /** 1101 * Returns the default fragment delimiter of the environment. 1102 * 1103 * @deprecated Use {@link Environment#getOption} with option "defaultFragmentDelimiter" 1104 * @returns {String} The fragment delimiter character 1105 */ 1106 1107 Environment.prototype.getDefaultFragmentDelimiter = function () { 1108 return this._options["defaultFragmentDelimiter"]; 1109 }; 1110 1111 /** 1112 * Sets the URI of the default schema for the environment. 1113 * 1114 * @deprecated Use {@link Environment#setOption} with option "defaultSchemaURI" 1115 * @param {String} uri The default schema URI 1116 */ 1117 1118 Environment.prototype.setDefaultSchemaURI = function (uri) { 1119 if (typeof uri === "string") { 1120 this._options["defaultSchemaURI"] = formatURI(uri); 1121 } 1122 }; 1123 1124 /** 1125 * Returns the default schema of the environment. 1126 * 1127 * @returns {JSONSchema} The default schema 1128 */ 1129 1130 Environment.prototype.getDefaultSchema = function () { 1131 return this.findSchema(this._options["defaultSchemaURI"]); 1132 }; 1133 1134 /** 1135 * Validates both the provided schema and the provided instance, and returns a {@link Report}. 1136 * If the schema fails to validate, the instance will not be validated. 1137 * 1138 * @param {JSONInstance|Any} instanceJSON The {@link JSONInstance} or JavaScript value to validate. 1139 * @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. 1140 * @returns {Report} The result of the validation 1141 */ 1142 1143 Environment.prototype.validate = function (instanceJSON, schemaJSON) { 1144 var instance, 1145 schema, 1146 schemaSchema, 1147 report = new Report(); 1148 1149 try { 1150 instance = this.createInstance(instanceJSON); 1151 report.instance = instance; 1152 } catch (e) { 1153 report.addError(e.uri, e.schemaUri, e.attribute, e.message, e.details); 1154 } 1155 1156 try { 1157 schema = this.createSchema(schemaJSON); 1158 report.schema = schema; 1159 1160 schemaSchema = schema.getSchema(); 1161 report.schemaSchema = schemaSchema; 1162 } catch (f) { 1163 report.addError(f.uri, f.schemaUri, f.attribute, f.message, f.details); 1164 } 1165 1166 if (schemaSchema) { 1167 schemaSchema.validate(schema, report); 1168 } 1169 1170 if (report.errors.length) { 1171 return report; 1172 } 1173 1174 return schema.validate(instance, report); 1175 }; 1176 1177 /** 1178 * @private 1179 */ 1180 1181 Environment.prototype._checkForInvalidInstances = function (stackSize, schemaURI) { 1182 var result = [], 1183 stack = [ 1184 [schemaURI, this._schemas[schemaURI]] 1185 ], 1186 counter = 0, 1187 item, uri, instance, properties, key; 1188 1189 while (counter++ < stackSize && stack.length) { 1190 item = stack.shift(); 1191 uri = item[0]; 1192 instance = item[1]; 1193 1194 if (instance instanceof JSONSchema) { 1195 if (this._schemas[instance._uri] !== instance) { 1196 result.push("Instance " + uri + " does not match " + instance._uri); 1197 } else { 1198 //schema = instance.getSchema(); 1199 //stack.push([uri + "/{schema}", schema]); 1200 1201 properties = instance.getAttributes(); 1202 for (key in properties) { 1203 if (properties[key] !== O[key]) { 1204 stack.push([uri + "/" + escapeURIComponent(key), properties[key]]); 1205 } 1206 } 1207 } 1208 } else if (typeOf(instance) === "object") { 1209 properties = instance; 1210 for (key in properties) { 1211 if (properties.hasOwnProperty(key)) { 1212 stack.push([uri + "/" + escapeURIComponent(key), properties[key]]); 1213 } 1214 } 1215 } else if (typeOf(instance) === "array") { 1216 properties = instance; 1217 for (key = 0; key < properties.length; ++key) { 1218 stack.push([uri + "/" + escapeURIComponent(key), properties[key]]); 1219 } 1220 } 1221 } 1222 1223 return result.length ? result : counter; 1224 }; 1225 1226 /** 1227 * A globaly accessible object that provides the ability to create and manage {@link Environments}, 1228 * as well as providing utility methods. 1229 * 1230 * @namespace 1231 */ 1232 1233 JSV = { 1234 _environments : {}, 1235 _defaultEnvironmentID : "", 1236 1237 /** 1238 * Returns if the provide value is an instance of {@link JSONInstance}. 1239 * 1240 * @param o The value to test 1241 * @returns {Boolean} If the provide value is an instance of {@link JSONInstance} 1242 */ 1243 1244 isJSONInstance : function (o) { 1245 return o instanceof JSONInstance; 1246 }, 1247 1248 /** 1249 * Returns if the provide value is an instance of {@link JSONSchema}. 1250 * 1251 * @param o The value to test 1252 * @returns {Boolean} If the provide value is an instance of {@link JSONSchema} 1253 */ 1254 1255 isJSONSchema : function (o) { 1256 return o instanceof JSONSchema; 1257 }, 1258 1259 /** 1260 * Creates and returns a new {@link Environment} that is a clone of the environment registered with the provided ID. 1261 * If no environment ID is provided, the default environment is cloned. 1262 * 1263 * @param {String} [id] The ID of the environment to clone. If <code>undefined</code>, the default environment ID is used. 1264 * @returns {Environment} A newly cloned {@link Environment} 1265 * @throws {Error} If there is no environment registered with the provided ID 1266 */ 1267 1268 createEnvironment : function (id) { 1269 id = id || this._defaultEnvironmentID; 1270 1271 if (!this._environments[id]) { 1272 throw new Error("Unknown Environment ID"); 1273 } 1274 //else 1275 return this._environments[id].clone(); 1276 }, 1277 1278 Environment : Environment, 1279 1280 /** 1281 * Registers the provided {@link Environment} with the provided ID. 1282 * 1283 * @param {String} id The ID of the environment 1284 * @param {Environment} env The environment to register 1285 */ 1286 1287 registerEnvironment : function (id, env) { 1288 id = id || (env || 0)._id; 1289 if (id && !this._environments[id] && env instanceof Environment) { 1290 env._id = id; 1291 this._environments[id] = env; 1292 } 1293 }, 1294 1295 /** 1296 * Sets which registered ID is the default environment. 1297 * 1298 * @param {String} id The ID of the registered environment that is default 1299 * @throws {Error} If there is no registered environment with the provided ID 1300 */ 1301 1302 setDefaultEnvironmentID : function (id) { 1303 if (typeof id === "string") { 1304 if (!this._environments[id]) { 1305 throw new Error("Unknown Environment ID"); 1306 } 1307 1308 this._defaultEnvironmentID = id; 1309 } 1310 }, 1311 1312 /** 1313 * Returns the ID of the default environment. 1314 * 1315 * @returns {String} The ID of the default environment 1316 */ 1317 1318 getDefaultEnvironmentID : function () { 1319 return this._defaultEnvironmentID; 1320 }, 1321 1322 // 1323 // Utility Functions 1324 // 1325 1326 /** 1327 * Returns the name of the type of the provided value. 1328 * 1329 * @event //utility 1330 * @param {Any} o The value to determine the type of 1331 * @returns {String} The name of the type of the value 1332 */ 1333 typeOf : typeOf, 1334 1335 /** 1336 * Return a new object that inherits all of the properties of the provided object. 1337 * 1338 * @event //utility 1339 * @param {Object} proto The prototype of the new object 1340 * @returns {Object} A new object that inherits all of the properties of the provided object 1341 */ 1342 createObject : createObject, 1343 1344 /** 1345 * Returns a new object with each property transformed by the iterator. 1346 * 1347 * @event //utility 1348 * @param {Object} obj The object to transform 1349 * @param {Function} iterator A function that returns the new value of the provided property 1350 * @param {Object} [scope] The value of <code>this</code> in the iterator 1351 * @returns {Object} A new object with each property transformed 1352 */ 1353 mapObject : mapObject, 1354 1355 /** 1356 * Returns a new array with each item transformed by the iterator. 1357 * 1358 * @event //utility 1359 * @param {Array} arr The array to transform 1360 * @param {Function} iterator A function that returns the new value of the provided item 1361 * @param {Object} scope The value of <code>this</code> in the iterator 1362 * @returns {Array} A new array with each item transformed 1363 */ 1364 mapArray : mapArray, 1365 1366 /** 1367 * Returns a new array that only contains the items allowed by the iterator. 1368 * 1369 * @event //utility 1370 * @param {Array} arr The array to filter 1371 * @param {Function} iterator The function that returns true if the provided property should be added to the array 1372 * @param {Object} scope The value of <code>this</code> within the iterator 1373 * @returns {Array} A new array that contains the items allowed by the iterator 1374 */ 1375 filterArray : filterArray, 1376 1377 /** 1378 * Returns the first index in the array that the provided item is located at. 1379 * 1380 * @event //utility 1381 * @param {Array} arr The array to search 1382 * @param {Any} o The item being searched for 1383 * @returns {Number} The index of the item in the array, or <code>-1</code> if not found 1384 */ 1385 searchArray : searchArray, 1386 1387 /** 1388 * Returns an array representation of a value. 1389 * <ul> 1390 * <li>For array-like objects, the value will be casted as an Array type.</li> 1391 * <li>If an array is provided, the function will simply return the same array.</li> 1392 * <li>For a null or undefined value, the result will be an empty Array.</li> 1393 * <li>For all other values, the value will be the first element in a new Array. </li> 1394 * </ul> 1395 * 1396 * @event //utility 1397 * @param {Any} o The value to convert into an array 1398 * @returns {Array} The value as an array 1399 */ 1400 toArray : toArray, 1401 1402 /** 1403 * Returns an array of the names of all properties of an object. 1404 * 1405 * @event //utility 1406 * @param {Object|Array} o The object in question 1407 * @returns {Array} The names of all properties 1408 */ 1409 keys : keys, 1410 1411 /** 1412 * Mutates the array by pushing the provided value onto the array only if it is not already there. 1413 * 1414 * @event //utility 1415 * @param {Array} arr The array to modify 1416 * @param {Any} o The object to add to the array if it is not already there 1417 * @returns {Array} The provided array for chaining 1418 */ 1419 pushUnique : pushUnique, 1420 1421 /** 1422 * Mutates the array by removing the first item that matches the provided value in the array. 1423 * 1424 * @event //utility 1425 * @param {Array} arr The array to modify 1426 * @param {Any} o The object to remove from the array 1427 * @returns {Array} The provided array for chaining 1428 */ 1429 popFirst : popFirst, 1430 1431 /** 1432 * Creates a copy of the target object. 1433 * <p> 1434 * This method will create a new instance of the target, and then mixin the properties of the target. 1435 * If <code>deep</code> is <code>true</code>, then each property will be cloned before mixin. 1436 * </p> 1437 * <p><b>Warning</b>: This is not a generic clone function, as it will only properly clone objects and arrays.</p> 1438 * 1439 * @event //utility 1440 * @param {Any} o The value to clone 1441 * @param {Boolean} [deep=false] If each property should be recursively cloned 1442 * @returns A cloned copy of the provided value 1443 */ 1444 clone : clone, 1445 1446 /** 1447 * Generates a pseudo-random UUID. 1448 * 1449 * @event //utility 1450 * @returns {String} A new universally unique ID 1451 */ 1452 randomUUID : randomUUID, 1453 1454 /** 1455 * Properly escapes a URI component for embedding into a URI string. 1456 * 1457 * @event //utility 1458 * @param {String} str The URI component to escape 1459 * @returns {String} The escaped URI component 1460 */ 1461 escapeURIComponent : escapeURIComponent, 1462 1463 /** 1464 * Returns a URI that is formated for JSV. Currently, this only ensures that the URI ends with a hash tag (<code>#</code>). 1465 * 1466 * @event //utility 1467 * @param {String} uri The URI to format 1468 * @returns {String} The URI formatted for JSV 1469 */ 1470 formatURI : formatURI, 1471 1472 /** 1473 * Merges two schemas/instance together. 1474 * 1475 * @event //utility 1476 * @param {JSONSchema|Any} base The old value to merge 1477 * @param {JSONSchema|Any} extra The new value to merge 1478 * @param {Boolean} extension If the merge is a JSON Schema extension 1479 * @return {Any} The modified base value 1480 */ 1481 1482 inherits : inherits, 1483 1484 /** 1485 * @private 1486 * @event //utility 1487 */ 1488 1489 InitializationError : InitializationError 1490 }; 1491 1492 this.JSV = JSV; //set global object 1493 exports.JSV = JSV; //export to CommonJS 1494 1495 require("./environments"); //load default environments 1496 1497 }());