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 3.2 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 randomUUID() { 165 return [ 166 I2H[Math.floor(Math.random() * 0x10)], 167 I2H[Math.floor(Math.random() * 0x10)], 168 I2H[Math.floor(Math.random() * 0x10)], 169 I2H[Math.floor(Math.random() * 0x10)], 170 I2H[Math.floor(Math.random() * 0x10)], 171 I2H[Math.floor(Math.random() * 0x10)], 172 I2H[Math.floor(Math.random() * 0x10)], 173 I2H[Math.floor(Math.random() * 0x10)], 174 "-", 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 "-4", //set 4 high bits of time_high field to version 180 I2H[Math.floor(Math.random() * 0x10)], 181 I2H[Math.floor(Math.random() * 0x10)], 182 I2H[Math.floor(Math.random() * 0x10)], 183 "-", 184 I2H[(Math.floor(Math.random() * 0x10) & 0x3) | 0x8], //specify 2 high bits of clock sequence 185 I2H[Math.floor(Math.random() * 0x10)], 186 I2H[Math.floor(Math.random() * 0x10)], 187 I2H[Math.floor(Math.random() * 0x10)], 188 "-", 189 I2H[Math.floor(Math.random() * 0x10)], 190 I2H[Math.floor(Math.random() * 0x10)], 191 I2H[Math.floor(Math.random() * 0x10)], 192 I2H[Math.floor(Math.random() * 0x10)], 193 I2H[Math.floor(Math.random() * 0x10)], 194 I2H[Math.floor(Math.random() * 0x10)], 195 I2H[Math.floor(Math.random() * 0x10)], 196 I2H[Math.floor(Math.random() * 0x10)], 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 ].join(""); 202 } 203 204 function escapeURIComponent(str) { 205 return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A'); 206 } 207 208 function formatURI(uri) { 209 if (typeof uri === "string" && uri.indexOf("#") === -1) { 210 uri += "#"; 211 } 212 return uri; 213 } 214 215 /** 216 * Defines an error, found by a schema, with an instance. 217 * This class can only be instantiated by {@link Report#addError}. 218 * 219 * @name ValidationError 220 * @class 221 * @see Report#addError 222 */ 223 224 /** 225 * The URI of the instance that has the error. 226 * 227 * @name ValidationError.prototype.uri 228 * @type String 229 */ 230 231 /** 232 * The URI of the schema that generated the error. 233 * 234 * @name ValidationError.prototype.schemaUri 235 * @type String 236 */ 237 238 /** 239 * The name of the schema attribute that generated the error. 240 * 241 * @name ValidationError.prototype.attribute 242 * @type String 243 */ 244 245 /** 246 * An user-friendly (English) message about what failed to validate. 247 * 248 * @name ValidationError.prototype.message 249 * @type String 250 */ 251 252 /** 253 * The value of the schema attribute that generated the error. 254 * 255 * @name ValidationError.prototype.details 256 * @type Any 257 */ 258 259 /** 260 * Reports are returned from validation methods to describe the result of a validation. 261 * 262 * @name Report 263 * @class 264 * @see JSONSchema#validate 265 * @see Environment#validate 266 */ 267 268 function Report() { 269 /** 270 * An array of {@link ValidationError} objects that define all the errors generated by the schema against the instance. 271 * 272 * @name Report.prototype.errors 273 * @type Array 274 * @see Report#addError 275 */ 276 this.errors = []; 277 278 /** 279 * A hash table of every instance and what schemas were validated against it. 280 * <p> 281 * The key of each item in the table is the URI of the instance that was validated. 282 * The value of this key is an array of strings of URIs of the schema that validated it. 283 * </p> 284 * 285 * @name Report.prototype.validated 286 * @type Object 287 * @see Report#registerValidation 288 * @see Report#isValidatedBy 289 */ 290 this.validated = {}; 291 292 /** 293 * If the report is generated by {@link Environment#validate}, this field is the generated instance. 294 * 295 * @name Report.prototype.instance 296 * @type JSONInstance 297 * @see Environment#validate 298 */ 299 300 /** 301 * If the report is generated by {@link Environment#validate}, this field is the generated schema. 302 * 303 * @name Report.prototype.schema 304 * @type JSONSchema 305 * @see Environment#validate 306 */ 307 308 /** 309 * If the report is generated by {@link Environment#validate}, this field is the schema's schema. 310 * This value is the same as calling <code>schema.getSchema()</code>. 311 * 312 * @name Report.prototype.schemaSchema 313 * @type JSONSchema 314 * @see Environment#validate 315 * @see JSONSchema#getSchema 316 */ 317 } 318 319 /** 320 * Adds a {@link ValidationError} object to the <a href="#errors"><code>errors</code></a> field. 321 * 322 * @param {JSONInstance} instance The instance that is invalid 323 * @param {JSONSchema} schema The schema that was validating the instance 324 * @param {String} attr The attribute that failed to validated 325 * @param {String} message A user-friendly message on why the schema attribute failed to validate the instance 326 * @param {Any} details The value of the schema attribute 327 */ 328 329 Report.prototype.addError = function (instance, schema, attr, message, details) { 330 this.errors.push({ 331 uri : instance.getURI(), 332 schemaUri : schema.getURI(), 333 attribute : attr, 334 message : message, 335 details : details 336 }); 337 }; 338 339 /** 340 * Registers that the provided instance URI has been validated by the provided schema URI. 341 * This is recorded in the <a href="#validated"><code>validated</code></a> field. 342 * 343 * @param {String} uri The URI of the instance that was validated 344 * @param {String} schemaUri The URI of the schema that validated the instance 345 */ 346 347 Report.prototype.registerValidation = function (uri, schemaUri) { 348 if (!this.validated[uri]) { 349 this.validated[uri] = [ schemaUri ]; 350 } else { 351 this.validated[uri].push(schemaUri); 352 } 353 }; 354 355 /** 356 * Returns if an instance with the provided URI has been validated by the schema with the provided URI. 357 * 358 * @param {String} uri The URI of the instance 359 * @param {String} schemaUri The URI of a schema 360 * @returns {Boolean} If the instance has been validated by the schema. 361 */ 362 363 Report.prototype.isValidatedBy = function (uri, schemaUri) { 364 return !!this.validated[uri] && searchArray(this.validated[uri], schemaUri) !== -1; 365 }; 366 367 /** 368 * A wrapper class for binding an Environment, URI and helper methods to an instance. 369 * This class is most commonly instantiated with {@link Environment#createInstance}. 370 * 371 * @name JSONInstance 372 * @class 373 * @param {Environment} env The environment this instance belongs to 374 * @param {JSONInstance|Any} json The value of the instance 375 * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. 376 * @param {String} [fd] The fragment delimiter for properties. If undefined, uses the environment default. 377 */ 378 379 function JSONInstance(env, json, uri, fd) { 380 if (json instanceof JSONInstance) { 381 if (typeof fd !== "string") { 382 fd = json._fd; 383 } 384 if (typeof uri !== "string") { 385 uri = json._uri; 386 } 387 json = json._value; 388 } 389 390 if (typeof uri !== "string") { 391 uri = "urn:uuid:" + randomUUID() + "#"; 392 } else if (uri.indexOf(":") === -1) { 393 uri = formatURI(URI.resolve("urn:uuid:" + randomUUID() + "#", uri)); 394 } 395 396 this._env = env; 397 this._value = json; 398 this._uri = uri; 399 this._fd = fd || this._env._options["defaultFragmentDelimiter"]; 400 } 401 402 /** 403 * Returns the environment the instance is bound to. 404 * 405 * @returns {Environment} The environment of the instance 406 */ 407 408 JSONInstance.prototype.getEnvironment = function () { 409 return this._env; 410 }; 411 412 /** 413 * Returns the name of the type of the instance. 414 * 415 * @returns {String} The name of the type of the instance 416 */ 417 418 JSONInstance.prototype.getType = function () { 419 return typeOf(this._value); 420 }; 421 422 /** 423 * Returns the JSON value of the instance. 424 * 425 * @returns {Any} The actual JavaScript value of the instance 426 */ 427 428 JSONInstance.prototype.getValue = function () { 429 return this._value; 430 }; 431 432 /** 433 * Returns the URI of the instance. 434 * 435 * @returns {String} The URI of the instance 436 */ 437 438 JSONInstance.prototype.getURI = function () { 439 return this._uri; 440 }; 441 442 /** 443 * Returns a resolved URI of a provided relative URI against the URI of the instance. 444 * 445 * @param {String} uri The relative URI to resolve 446 * @returns {String} The resolved URI 447 */ 448 449 JSONInstance.prototype.resolveURI = function (uri) { 450 return formatURI(URI.resolve(this._uri, uri)); 451 }; 452 453 /** 454 * Returns an array of the names of all the properties. 455 * 456 * @returns {Array} An array of strings which are the names of all the properties 457 */ 458 459 JSONInstance.prototype.getPropertyNames = function () { 460 return keys(this._value); 461 }; 462 463 /** 464 * Returns a {@link JSONInstance} of the value of the provided property name. 465 * 466 * @param {String} key The name of the property to fetch 467 * @returns {JSONInstance} The instance of the property value 468 */ 469 470 JSONInstance.prototype.getProperty = function (key) { 471 var value = this._value ? this._value[key] : undefined; 472 if (value instanceof JSONInstance) { 473 return value; 474 } 475 //else 476 return new JSONInstance(this._env, value, this._uri + this._fd + escapeURIComponent(key), this._fd); 477 }; 478 479 /** 480 * Returns all the property instances of the target instance. 481 * <p> 482 * If the target instance is an Object, then the method will return a hash table of {@link JSONInstance}s of all the properties. 483 * If the target instance is an Array, then the method will return an array of {@link JSONInstance}s of all the items. 484 * </p> 485 * 486 * @returns {Object|Array|undefined} The list of instances for all the properties 487 */ 488 489 JSONInstance.prototype.getProperties = function () { 490 var type = typeOf(this._value), 491 self = this; 492 493 if (type === "object") { 494 return mapObject(this._value, function (value, key) { 495 if (value instanceof JSONInstance) { 496 return value; 497 } 498 return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd); 499 }); 500 } else if (type === "array") { 501 return mapArray(this._value, function (value, key) { 502 if (value instanceof JSONInstance) { 503 return value; 504 } 505 return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd); 506 }); 507 } 508 }; 509 510 /** 511 * Returns the JSON value of the provided property name. 512 * This method is a faster version of calling <code>instance.getProperty(key).getValue()</code>. 513 * 514 * @param {String} key The name of the property 515 * @returns {Any} The JavaScript value of the instance 516 * @see JSONInstance#getProperty 517 * @see JSONInstance#getValue 518 */ 519 520 JSONInstance.prototype.getValueOfProperty = function (key) { 521 if (this._value) { 522 if (this._value[key] instanceof JSONInstance) { 523 return this._value[key]._value; 524 } 525 return this._value[key]; 526 } 527 }; 528 529 /** 530 * Return if the provided value is the same as the value of the instance. 531 * 532 * @param {JSONInstance|Any} instance The value to compare 533 * @returns {Boolean} If both the instance and the value match 534 */ 535 536 JSONInstance.prototype.equals = function (instance) { 537 if (instance instanceof JSONInstance) { 538 return this._value === instance._value; 539 } 540 //else 541 return this._value === instance; 542 }; 543 544 /** 545 * Warning: Not a generic clone function 546 * Produces a JSV acceptable clone 547 */ 548 549 function clone(obj, deep) { 550 var newObj, x; 551 552 if (obj instanceof JSONInstance) { 553 obj = obj.getValue(); 554 } 555 556 switch (typeOf(obj)) { 557 case "object": 558 if (deep) { 559 newObj = {}; 560 for (x in obj) { 561 if (obj[x] !== O[x]) { 562 newObj[x] = clone(obj[x], deep); 563 } 564 } 565 return newObj; 566 } else { 567 return createObject(obj); 568 } 569 break; 570 case "array": 571 if (deep) { 572 newObj = new Array(obj.length); 573 x = obj.length; 574 while (--x >= 0) { 575 newObj[x] = clone(obj[x], deep); 576 } 577 return newObj; 578 } else { 579 return Array.pototype.slice.call(obj); 580 } 581 break; 582 default: 583 return obj; 584 } 585 } 586 587 /** 588 * This class binds a {@link JSONInstance} with a {@link JSONSchema} to provided context aware methods. 589 * 590 * @name JSONSchema 591 * @class 592 * @param {Environment} env The environment this schema belongs to 593 * @param {JSONInstance|Any} json The value of the schema 594 * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. 595 * @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. 596 * @extends JSONInstance 597 */ 598 599 function JSONSchema(env, json, uri, schema) { 600 var fr; 601 JSONInstance.call(this, env, json, uri); 602 603 if (schema === true) { 604 this._schema = this; 605 } else if (json instanceof JSONSchema && !(schema instanceof JSONSchema)) { 606 this._schema = json._schema; //TODO: Make sure cross environments don't mess everything up 607 } else { 608 this._schema = schema instanceof JSONSchema ? schema : this._env.getDefaultSchema() || JSONSchema.createEmptySchema(this._env); 609 } 610 611 //determine fragment delimiter from schema 612 fr = this._schema.getValueOfProperty("fragmentResolution"); 613 if (fr === "dot-delimited") { 614 this._fd = "."; 615 } else if (fr === "slash-delimited") { 616 this._fd = "/"; 617 } 618 } 619 620 JSONSchema.prototype = createObject(JSONInstance.prototype); 621 622 /** 623 * Creates an empty schema. 624 * 625 * @param {Environment} env The environment of the schema 626 * @returns {JSONSchema} The empty schema, who's schema is itself. 627 */ 628 629 JSONSchema.createEmptySchema = function (env) { 630 var schema = createObject(JSONSchema.prototype); 631 JSONInstance.call(schema, env, {}, undefined, undefined); 632 schema._schema = schema; 633 return schema; 634 }; 635 636 /** 637 * Returns the schema of the schema. 638 * 639 * @returns {JSONSchema} The schema of the schema 640 */ 641 642 JSONSchema.prototype.getSchema = function () { 643 return this._schema; 644 }; 645 646 /** 647 * Returns the value of the provided attribute name. 648 * <p> 649 * This method is different from {@link JSONInstance#getProperty} as the named property 650 * is converted using a parser defined by the schema's schema before being returned. This 651 * makes the return value of this method attribute dependent. 652 * </p> 653 * 654 * @param {String} key The name of the attribute 655 * @param {Any} [arg] Some attribute parsers accept special arguments for returning resolved values. This is attribute dependent. 656 * @returns {JSONSchema|Any} The value of the attribute 657 */ 658 659 JSONSchema.prototype.getAttribute = function (key, arg) { 660 if (this._attributes && !arg) { 661 return this._attributes[key]; 662 } 663 664 var schemaProperty = this._schema.getProperty("properties").getProperty(key), 665 parser = schemaProperty.getValueOfProperty("parser"), 666 property = this.getProperty(key); 667 if (typeof parser === "function") { 668 return parser(property, schemaProperty, arg); 669 } 670 //else 671 return property.getValue(); 672 }; 673 674 /** 675 * Returns all the attributes of the schema. 676 * 677 * @returns {Object} A map of all parsed attribute values 678 */ 679 680 JSONSchema.prototype.getAttributes = function () { 681 var properties, schemaProperties, key, schemaProperty, parser; 682 683 if (!this._attributes && this.getType() === "object") { 684 properties = this.getProperties(); 685 schemaProperties = this._schema.getProperty("properties"); 686 this._attributes = {}; 687 for (key in properties) { 688 if (properties[key] !== O[key]) { 689 schemaProperty = schemaProperties && schemaProperties.getProperty(key); 690 parser = schemaProperty && schemaProperty.getValueOfProperty("parser"); 691 if (typeof parser === "function") { 692 this._attributes[key] = parser(properties[key], schemaProperty); 693 } else { 694 this._attributes[key] = properties[key].getValue(); 695 } 696 } 697 } 698 } 699 700 return clone(this._attributes, false); 701 }; 702 703 /** 704 * Convenience method for retrieving a link or link object from a schema. 705 * This method is the same as calling <code>schema.getAttribute("links", [rel, instance])[0];</code>. 706 * 707 * @param {String} rel The link relationship 708 * @param {JSONInstance} [instance] The instance to resolve any URIs from 709 * @returns {String|Object|undefined} If <code>instance</code> is provided, a string containing the resolve URI of the link is returned. 710 * If <code>instance</code> is not provided, a link object is returned with details of the link. 711 * If no link with the provided relationship exists, <code>undefined</code> is returned. 712 * @see JSONSchema#getAttribute 713 */ 714 715 JSONSchema.prototype.getLink = function (rel, instance) { 716 var schemaLinks = this.getAttribute("links", [rel, instance]); 717 if (schemaLinks && schemaLinks.length && schemaLinks[schemaLinks.length - 1]) { 718 return schemaLinks[schemaLinks.length - 1]; 719 } 720 }; 721 722 /** 723 * Validates the provided instance against the target schema and returns a {@link Report}. 724 * 725 * @param {JSONInstance|Any} instance The instance to validate; may be a {@link JSONInstance} or any JavaScript value 726 * @param {Report} [report] A {@link Report} to concatenate the result of the validation to. If <code>undefined</code>, a new {@link Report} is created. 727 * @param {JSONInstance} [parent] The parent/containing instance of the provided instance 728 * @param {JSONSchema} [parentSchema] The schema of the parent/containing instance 729 * @param {String} [name] The name of the parent object's property that references the instance 730 * @returns {Report} The result of the validation 731 */ 732 733 JSONSchema.prototype.validate = function (instance, report, parent, parentSchema, name) { 734 var validator = this._schema.getValueOfProperty("validator"); 735 736 if (!(instance instanceof JSONInstance)) { 737 instance = this.getEnvironment().createInstance(instance); 738 } 739 740 if (!(report instanceof Report)) { 741 report = new Report(); 742 } 743 744 if (typeof validator === "function" && !report.isValidatedBy(instance.getURI(), this.getURI())) { 745 report.registerValidation(instance.getURI(), this.getURI()); 746 validator(instance, this, this._schema, report, parent, parentSchema, name); 747 } 748 749 return report; 750 }; 751 752 /** 753 * Merges two schemas/instances together. 754 */ 755 756 function inherits(base, extra, extension) { 757 var baseType = base instanceof JSONSchema ? "schema" : typeOf(base), 758 extraType = extra instanceof JSONSchema ? "schema" : typeOf(extra), 759 child, x; 760 761 if (extraType === "undefined") { 762 return clone(base, true); 763 } else if (baseType === "undefined" || extraType !== baseType) { 764 return clone(extra, true); 765 } else if (extraType === "object" || extraType === "schema") { 766 if (baseType === "schema") { 767 base = base.getAttributes(); 768 extra = extra.getAttributes(); 769 if (extra["extends"] && extension && extra["extends"] instanceof JSONSchema) { 770 extra["extends"] = [ extra["extends"] ]; 771 } 772 } 773 child = clone(base, true); //this could be optimized as some properties get overwritten 774 for (x in extra) { 775 if (extra[x] !== O[x]) { 776 child[x] = inherits(base[x], extra[x], extension); 777 } 778 } 779 return child; 780 } else { 781 return clone(extra, true); 782 } 783 } 784 785 /** 786 * An Environment is a sandbox of schemas thats behavior is different from other environments. 787 * 788 * @name Environment 789 * @class 790 */ 791 792 function Environment() { 793 this._id = randomUUID(); 794 this._schemas = {}; 795 this._options = {}; 796 } 797 798 /** 799 * Returns a clone of the target environment. 800 * 801 * @returns {Environment} A new {@link Environment} that is a exact copy of the target environment 802 */ 803 804 Environment.prototype.clone = function () { 805 var env = new Environment(); 806 env._schemas = createObject(this._schemas); 807 env._options = createObject(this._options); 808 809 return env; 810 }; 811 812 /** 813 * Returns a new {@link JSONInstance} of the provided data. 814 * 815 * @param {JSONInstance|Any} data The value of the instance 816 * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. 817 * @returns {JSONInstance} A new {@link JSONInstance} from the provided data 818 */ 819 820 Environment.prototype.createInstance = function (data, uri) { 821 var instance; 822 uri = formatURI(uri); 823 824 if (data instanceof JSONInstance && (!uri || data.getURI() === uri)) { 825 return data; 826 } 827 //else 828 instance = new JSONInstance(this, data, uri); 829 830 return instance; 831 }; 832 833 /** 834 * Creates a new {@link JSONSchema} from the provided data, and registers it with the environment. 835 * 836 * @param {JSONInstance|Any} data The value of the schema 837 * @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. 838 * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. 839 * @returns {JSONSchema} A new {@link JSONSchema} from the provided data 840 */ 841 842 Environment.prototype.createSchema = function (data, schema, uri) { 843 var instance, 844 initializer; 845 uri = formatURI(uri); 846 847 if (data instanceof JSONSchema && (!uri || data._uri === uri) && (!schema || data._schema.equals(schema))) { 848 return data; 849 } 850 851 instance = new JSONSchema(this, data, uri, schema); 852 853 initializer = instance.getSchema().getValueOfProperty("initializer"); 854 if (typeof initializer === "function") { 855 instance = initializer(instance); 856 } 857 858 //register schema 859 this._schemas[instance._uri] = instance; 860 861 //build & cache the rest of the schema 862 instance.getAttributes(); 863 864 return instance; 865 }; 866 867 /** 868 * Creates an empty schema. 869 * 870 * @param {Environment} env The environment of the schema 871 * @returns {JSONSchema} The empty schema, who's schema is itself. 872 */ 873 874 Environment.prototype.createEmptySchema = function () { 875 return JSONSchema.createEmptySchema(this); 876 }; 877 878 /** 879 * Returns the schema registered with the provided URI. 880 * 881 * @param {String} uri The absolute URI of the required schema 882 * @returns {JSONSchema|undefined} The request schema, or <code>undefined</code> if not found 883 */ 884 885 Environment.prototype.findSchema = function (uri) { 886 return this._schemas[formatURI(uri)]; 887 }; 888 889 /** 890 * Sets the specified environment option to the specified value. 891 * 892 * @param {String} name The name of the environment option to set 893 * @param {Any} value The new value of the environment option 894 */ 895 896 Environment.prototype.setOption = function (name, value) { 897 this._options[name] = value; 898 }; 899 900 /** 901 * Returns the specified environment option. 902 * 903 * @param {String} name The name of the environment option to set 904 * @returns {Any} The value of the environment option 905 */ 906 907 Environment.prototype.getOption = function (name) { 908 return this._options[name]; 909 }; 910 911 /** 912 * Sets the default fragment delimiter of the environment. 913 * 914 * @deprecated Use {@link Environment#setOption} with option "defaultFragmentDelimiter" 915 * @param {String} fd The fragment delimiter character 916 */ 917 918 Environment.prototype.setDefaultFragmentDelimiter = function (fd) { 919 if (typeof fd === "string" && fd.length > 0) { 920 this._options["defaultFragmentDelimiter"] = fd; 921 } 922 }; 923 924 /** 925 * Returns the default fragment delimiter of the environment. 926 * 927 * @deprecated Use {@link Environment#getOption} with option "defaultFragmentDelimiter" 928 * @returns {String} The fragment delimiter character 929 */ 930 931 Environment.prototype.getDefaultFragmentDelimiter = function () { 932 return this._options["defaultFragmentDelimiter"]; 933 }; 934 935 /** 936 * Sets the URI of the default schema for the environment. 937 * 938 * @deprecated Use {@link Environment#setOption} with option "defaultSchemaURI" 939 * @param {String} uri The default schema URI 940 */ 941 942 Environment.prototype.setDefaultSchemaURI = function (uri) { 943 if (typeof uri === "string") { 944 this._options["defaultSchemaURI"] = formatURI(uri); 945 } 946 }; 947 948 /** 949 * Returns the default schema of the environment. 950 * 951 * @returns {JSONSchema} The default schema 952 */ 953 954 Environment.prototype.getDefaultSchema = function () { 955 return this.findSchema(this._options["defaultSchemaURI"]); 956 }; 957 958 /** 959 * Validates both the provided schema and the provided instance, and returns a {@link Report}. 960 * If the schema fails to validate, the instance will not be validated. 961 * 962 * @param {JSONInstance|Any} instanceJSON The {@link JSONInstance} or JavaScript value to validate. 963 * @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. 964 * @returns {Report} The result of the validation 965 */ 966 967 Environment.prototype.validate = function (instanceJSON, schemaJSON) { 968 var instance = this.createInstance(instanceJSON), 969 schema = this.createSchema(schemaJSON), 970 schemaSchema = schema.getSchema(), 971 report = new Report(); 972 973 report.instance = instance; 974 report.schema = schema; 975 report.schemaSchema = schemaSchema; 976 977 schemaSchema.validate(schema, report); 978 979 if (report.errors.length) { 980 return report; 981 } 982 983 return schema.validate(instance, report); 984 }; 985 986 987 /** 988 * A globaly accessible object that provides the ability to create and manage {@link Environments}, 989 * as well as providing utility methods. 990 * 991 * @namespace 992 */ 993 994 JSV = { 995 _environments : {}, 996 _defaultEnvironmentID : "", 997 998 /** 999 * Returns if the provide value is an instance of {@link JSONInstance}. 1000 * 1001 * @param o The value to test 1002 * @returns {Boolean} If the provide value is an instance of {@link JSONInstance} 1003 */ 1004 1005 isJSONInstance : function (o) { 1006 return o instanceof JSONInstance; 1007 }, 1008 1009 /** 1010 * Returns if the provide value is an instance of {@link JSONSchema}. 1011 * 1012 * @param o The value to test 1013 * @returns {Boolean} If the provide value is an instance of {@link JSONSchema} 1014 */ 1015 1016 isJSONSchema : function (o) { 1017 return o instanceof JSONSchema; 1018 }, 1019 1020 /** 1021 * Creates and returns a new {@link Environment} that is a clone of the environment registered with the provided ID. 1022 * If no environment ID is provided, the default environment is cloned. 1023 * 1024 * @param {String} [id] The ID of the environment to clone. If <code>undefined</code>, the default environment ID is used. 1025 * @returns {Environment} A newly cloned {@link Environment} 1026 * @throws {Error} If there is no environment registered with the provided ID 1027 */ 1028 1029 createEnvironment : function (id) { 1030 id = id || this._defaultEnvironmentID; 1031 1032 if (!this._environments[id]) { 1033 throw new Error("Unknown Environment ID"); 1034 } 1035 //else 1036 return this._environments[id].clone(); 1037 }, 1038 1039 Environment : Environment, 1040 1041 /** 1042 * Registers the provided {@link Environment} with the provided ID. 1043 * 1044 * @param {String} id The ID of the environment 1045 * @param {Environment} env The environment to register 1046 */ 1047 1048 registerEnvironment : function (id, env) { 1049 id = id || (env || 0)._id; 1050 if (id && !this._environments[id] && env instanceof Environment) { 1051 env._id = id; 1052 this._environments[id] = env; 1053 } 1054 }, 1055 1056 /** 1057 * Sets which registered ID is the default environment. 1058 * 1059 * @param {String} id The ID of the registered environment that is default 1060 * @throws {Error} If there is no registered environment with the provided ID 1061 */ 1062 1063 setDefaultEnvironmentID : function (id) { 1064 if (typeof id === "string") { 1065 if (!this._environments[id]) { 1066 throw new Error("Unknown Environment ID"); 1067 } 1068 1069 this._defaultEnvironmentID = id; 1070 } 1071 }, 1072 1073 /** 1074 * Returns the ID of the default environment. 1075 * 1076 * @returns {String} The ID of the default environment 1077 */ 1078 1079 getDefaultEnvironmentID : function () { 1080 return this._defaultEnvironmentID; 1081 }, 1082 1083 // 1084 // Utility Functions 1085 // 1086 1087 /** 1088 * Returns the name of the type of the provided value. 1089 * 1090 * @event //utility 1091 * @param {Any} o The value to determine the type of 1092 * @returns {String} The name of the type of the value 1093 */ 1094 typeOf : typeOf, 1095 1096 /** 1097 * Return a new object that inherits all of the properties of the provided object. 1098 * 1099 * @event //utility 1100 * @param {Object} proto The prototype of the new object 1101 * @returns {Object} A new object that inherits all of the properties of the provided object 1102 */ 1103 createObject : createObject, 1104 1105 /** 1106 * Returns a new object with each property transformed by the iterator. 1107 * 1108 * @event //utility 1109 * @param {Object} obj The object to transform 1110 * @param {Function} iterator A function that returns the new value of the provided property 1111 * @param {Object} [scope] The value of <code>this</code> in the iterator 1112 * @returns {Object} A new object with each property transformed 1113 */ 1114 mapObject : mapObject, 1115 1116 /** 1117 * Returns a new array with each item transformed by the iterator. 1118 * 1119 * @event //utility 1120 * @param {Array} arr The array to transform 1121 * @param {Function} iterator A function that returns the new value of the provided item 1122 * @param {Object} scope The value of <code>this</code> in the iterator 1123 * @returns {Array} A new array with each item transformed 1124 */ 1125 mapArray : mapArray, 1126 1127 /** 1128 * Returns a new array that only contains the items allowed by the iterator. 1129 * 1130 * @event //utility 1131 * @param {Array} arr The array to filter 1132 * @param {Function} iterator The function that returns true if the provided property should be added to the array 1133 * @param {Object} scope The value of <code>this</code> within the iterator 1134 * @returns {Array} A new array that contains the items allowed by the iterator 1135 */ 1136 filterArray : filterArray, 1137 1138 /** 1139 * Returns the first index in the array that the provided item is located at. 1140 * 1141 * @event //utility 1142 * @param {Array} arr The array to search 1143 * @param {Any} o The item being searched for 1144 * @returns {Number} The index of the item in the array, or <code>-1</code> if not found 1145 */ 1146 searchArray : searchArray, 1147 1148 /** 1149 * Returns an array representation of a value. 1150 * <ul> 1151 * <li>For array-like objects, the value will be casted as an Array type.</li> 1152 * <li>If an array is provided, the function will simply return the same array.</li> 1153 * <li>For a null or undefined value, the result will be an empty Array.</li> 1154 * <li>For all other values, the value will be the first element in a new Array. </li> 1155 * </ul> 1156 * 1157 * @event //utility 1158 * @param {Any} o The value to convert into an array 1159 * @returns {Array} The value as an array 1160 */ 1161 toArray : toArray, 1162 1163 /** 1164 * Returns an array of the names of all properties of an object. 1165 * 1166 * @event //utility 1167 * @param {Object|Array} o The object in question 1168 * @returns {Array} The names of all properties 1169 */ 1170 keys : keys, 1171 1172 /** 1173 * Mutates the array by pushing the provided value onto the array only if it is not already there. 1174 * 1175 * @event //utility 1176 * @param {Array} arr The array to modify 1177 * @param {Any} o The object to add to the array if it is not already there 1178 * @returns {Array} The provided array for chaining 1179 */ 1180 pushUnique : pushUnique, 1181 1182 /** 1183 * Creates a copy of the target object. 1184 * <p> 1185 * This method will create a new instance of the target, and then mixin the properties of the target. 1186 * If <code>deep</code> is <code>true</code>, then each property will be cloned before mixin. 1187 * </p> 1188 * <p><b>Warning</b>: This is not a generic clone function, as it will only properly clone objects and arrays.</p> 1189 * 1190 * @event //utility 1191 * @param {Any} o The value to clone 1192 * @param {Boolean} [deep=false] If each property should be recursively cloned 1193 * @returns A cloned copy of the provided value 1194 */ 1195 clone : clone, 1196 1197 /** 1198 * Generates a pseudo-random UUID. 1199 * 1200 * @event //utility 1201 * @returns {String} A new universally unique ID 1202 */ 1203 randomUUID : randomUUID, 1204 1205 /** 1206 * Properly escapes a URI component for embedding into a URI string. 1207 * 1208 * @event //utility 1209 * @param {String} str The URI component to escape 1210 * @returns {String} The escaped URI component 1211 */ 1212 escapeURIComponent : escapeURIComponent, 1213 1214 /** 1215 * Returns a URI that is formated for JSV. Currently, this only ensures that the URI ends with a hash tag (<code>#</code>). 1216 * 1217 * @event //utility 1218 * @param {String} uri The URI to format 1219 * @returns {String} The URI formatted for JSV 1220 */ 1221 formatURI : formatURI, 1222 1223 /** 1224 * Merges two schemas/instance together. 1225 * 1226 * @event //utility 1227 * @param {JSONSchema|Any} base The old value to merge 1228 * @param {JSONSchema|Any} extra The new value to merge 1229 * @param {Boolean} extension If the merge is a JSON Schema extension 1230 * @return {Any} The modified base value 1231 */ 1232 1233 inherits : inherits 1234 }; 1235 1236 this.JSV = JSV; //set global object 1237 exports.JSV = JSV; //export to CommonJS 1238 1239 require("./environments"); //load default environments 1240 1241 }());