UNPKG

@nodecg/json-schema-lib

Version:

Extensible JSON Schema library with support for multi-file schemas using $ref

1,778 lines (1,539 loc) 80.1 kB
/*! * Json Schema Lib v0.0.6 (August 16th 2017) * * https://github.com/BigstickCarpet/json-schema-lib * * @author James Messinger (http://jamesmessinger.com) * @license MIT */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.jsonSchemaLib = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var typeOf = require('../util/typeOf'); var deepAssign = require('../util/deepAssign'); module.exports = Config; /** * Config that determine how {@link JsonSchemaLib} behaves * * @param {object} [config] - User-specified config. These override the defaults. * @param {object} [defaultConfig] - The default config to use instead of {@link Config.defaults} * * @class */ function Config (config, defaultConfig) { validateConfig(config); deepAssign(this, defaultConfig || Config.defaults); if (config) { deepAssign(this, config); } } /** * The default configuration. */ Config.defaults = { /** * The Promise class to use when asynchronous methods are called without a callback. * Users can override this with a custom Promise implementation, such as Bluebird * or a polyfill. * * @type {function} */ Promise: require('../util/Promise'), /** * Options for downloading files via HTTP and HTTPS. * These options are used by the HttpPlugin and the XhrPlugin. */ http: { /** * HTTP headers to send when making HTTP requests. */ headers: {}, /** * The maximum amount of time (in milliseconds) to wait for an HTTP response. * * @type {number} */ timeout: 5000, /** * The maximum number of HTTP redirects to follow. * If set to zero, then no redirects will be followed. * * NOTE: This option only applies to Node.js. In a web browser, redirects are automatically * followed, and there is no way to disable or limit this. * * @type {number} */ maxRedirects: 5, /** * Determines whether HTTP requests should include credentials, such as cookies, * authorization headers, TLS certificates, etc. * * NOTE: This option only applies to web browsers, not Node.js. * * @type {boolean} */ withCredentials: false, }, }; /** * Ensures that a user-supplied value is a valid configuration POJO. * An error is thrown if the value is invalid. * * @param {*} config - The user-supplied value to validate */ function validateConfig (config) { var type = typeOf(config); if (type.hasValue && !type.isPOJO) { throw ono('Invalid arguments. Expected a configuration object.'); } } },{"../util/Promise":23,"../util/deepAssign":26,"../util/typeOf":34,"ono":37}],2:[function(require,module,exports){ 'use strict'; var omit = require('../util/omit'); var __internal = require('../util/internal'); module.exports = File; /** * Contains information about a file, such as its path, type, and contents. * * @param {Schema} schema - The JSON Schema that the file is part of * * @class */ function File (schema) { /** * The {@link Schema} that this file belongs to. * * @type {Schema} */ this.schema = schema; /** * The file's full (absolute) URL, without any hash * * @type {string} */ this.url = ''; /** * The file's data. This can be any data type, including a string, object, array, binary, etc. * * @type {*} */ this.data = undefined; /** * The file's MIME type (e.g. "application/json", "text/html", etc.), if known. * * @type {?string} */ this.mimeType = undefined; /** * The file's encoding (e.g. "utf-8", "iso-8859-2", "windows-1251", etc.), if known * * @type {?string} */ this.encoding = undefined; /** * Internal stuff. Use at your own risk! * * @private */ this[__internal] = { /** * Keeps track of the state of each file as the schema is being read. * * @type {number} */ state: 0, }; } /** * Returns a human-friendly representation of the File object. * * @returns {string} */ File.prototype.toString = function toString () { return this.path; }; /** * Serializes the {@link File} instance * * @returns {object} */ File.prototype.toJSON = function toJSON () { return omit(this, 'schema', __internal); }; },{"../util/internal":27,"../util/omit":30}],3:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var File = require('./File'); var assign = require('../util/assign'); var __internal = require('../util/internal'); module.exports = FileArray; /** * An array of {@link File} objects, with some helper methods. * * @param {Schema} schema - The JSON Schema that these files are part of * * @class * @extends Array */ function FileArray (schema) { var fileArray = []; /** * Internal stuff. Use at your own risk! * * @private */ fileArray[__internal] = { /** * A reference to the {@link Schema} object */ schema: schema, }; // Return an array that "inherits" from FileArray return assign(fileArray, FileArray.prototype); } /** * Determines whether a given file is in the array. * * @param {string|File} url * An absolute URL, or a relative URL (relative to the schema's root file), or a {@link File} object * * @returns {boolean} */ FileArray.prototype.exists = function exists (url) { if (this.length === 0) { return false; } // Get the absolute URL var absoluteURL = resolveURL(url, this[__internal].schema); // Try to find a file with this URL for (var i = 0; i < this.length; i++) { var file = this[i]; if (file.url === absoluteURL) { return true; } } // If we get here, thne no files matched the URL return false; }; /** * Returns the given file in the array. Throws an error if not found. * * @param {string|File} url * An absolute URL, or a relative URL (relative to the schema's root file), or a {@link File} object * * @returns {File} */ FileArray.prototype.get = function get (url) { if (this.length === 0) { throw ono('Unable to get %s. \nThe schema is empty.', url); } // Get the absolute URL var absoluteURL = resolveURL(url, this[__internal].schema); // Try to find a file with this URL for (var i = 0; i < this.length; i++) { var file = this[i]; if (file.url === absoluteURL) { return file; } } // If we get here, then no files matched the URL throw ono('Unable to get %s. \nThe schema does not include this file.', absoluteURL); }; /** * Resolves the given URL to an absolute URL. * * @param {string|File} url * An absolute URL, or a relative URL (relative to the schema's root file), or a {@link File} object * * @param {Schema} schema * @returns {boolean} */ function resolveURL (url, schema) { if (url instanceof File) { // The URL is already absolute return url.url; } return schema.plugins.resolveURL({ from: schema.rootURL, to: url }); } },{"../util/assign":25,"../util/internal":27,"./File":2,"ono":37}],4:[function(require,module,exports){ 'use strict'; var Config = require('../Config'); var PluginManager = require('../PluginManager'); var read = require('./read'); var normalizeArgs = require('./normalizeArgs'); module.exports = JsonSchemaLib; /** * The public JsonSchemaLib API. * * @param {Config} [config] - The configuration to use. Can be overridden by {@link JsonSchemaLib#read} * @param {object[]} [plugins] - The plugins to use. Additional plugins can be added via {@link JsonSchemaLib#use} * * @class */ function JsonSchemaLib (config, plugins) { if (plugins === undefined && Array.isArray(config)) { plugins = config; config = undefined; } /** * The configuration for this instance of {@link JsonSchemaLib}. * * @type {Config} */ this.config = new Config(config); /** * The plugins that have been added to this instance of {@link JsonSchemaLib} * * @type {object[]} */ this.plugins = new PluginManager(plugins); } /** * Adds a plugin to this {@link JsonSchemaLib} instance. * * @param {object} plugin - A plugin object * @param {number} [priority] - Optionaly override the plugin's default priority. */ JsonSchemaLib.prototype.use = function use (plugin, priority) { this.plugins.use(plugin, priority); }; /** * Serializes the {@link JsonSchemaLib} instance * * @returns {object} */ JsonSchemaLib.prototype.toJSON = function toJSON () { return { config: this.config, plugins: this.plugins, }; }; /** * Synchronously reads the given file, URL, or data, including any other files or URLs that are * referneced by JSON References ($ref). * * @param {string} [url] * The file path or URL of the JSON schema * * @param {object|string} [data] * The JSON schema, as an object, or as a JSON/YAML string. If you omit this, then the data will * be read from `url` instead. * * @param {Config} [config] * Config that determine how the schema will be read * * @returns {Schema} */ JsonSchemaLib.prototype.readSync = function readSync (url, data, config) { var args = normalizeArgs(arguments); var error = args.error; url = args.url; data = args.data; config = args.config; config.sync = true; if (error) { // The arguments are invalid throw error; } else { var e, s; // Call `read()` synchronously, and capture the result read.call(this, url, data, config, function (err, schema) { e = err; s = schema; }); // Return the result synchronously if (e) { throw e; } else { return s; } } }; /** * Asynchronously reads the given file, URL, or data, including any other files or URLs that are * referneced by JSON References ($ref). * * @param {string} [url] * The file path or URL of the JSON schema * * @param {object|string} [data] * The JSON schema, as an object, or as a JSON/YAML string. If you omit this, then the data will * be read from `url` instead. * * @param {Config} [config] * Config that determine how the schema will be read * * @param {function} [callback] * An error-first callback. If not specified, then a Promise will be returned. * * @returns {Promise<Schema>|undefined} */ JsonSchemaLib.prototype.read = JsonSchemaLib.prototype.readAsync = function readAsync (url, data, config, callback) { var args = normalizeArgs(arguments); var error = args.error; var me = this; url = args.url; data = args.data; config = args.config; callback = args.callback; config.sync = false; if (error) { // The arguments are invalid if (callback) { callAsync(callback, error); } else { return config.Promise.reject(error); } } else if (callback) { try { // Call `read()`, and forward the result to the callback read.call(this, url, data, config, function (err, schema) { callAsync(callback, err, schema); }); } catch (err) { // `read()` threw an error, so forward it to the callback callAsync(callback, err); } } else { // Wrap `read()` in a Promise return new config.Promise(function (resolve, reject) { read.call(me, url, data, config, function (err, schema) { if (err) { reject(err); } else { resolve(schema); } }); }); } }; /** * Calls the callback function, wrapping it in a `setTimeout` to ensure that it's not * called synchronously. * * @param {function} fn * @param {Error} [error] * @param {Schema} [schema] */ function callAsync (fn, error, schema) { setTimeout(function () { fn(error, schema); }, 0); } },{"../Config":1,"../PluginManager":14,"./normalizeArgs":5,"./read":6}],5:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var Config = require('../Config'); module.exports = normalizeArgs; /** * Normalizes arguments for the {@link JsonSchemaLib#readAsync} and {@link JsonSchemaLib#readSync} * methods, accounting for optional args and defaults. * * NOTE: This function does NOT throw errors. The calling code is responsible for checking the * `error` property of the returned object, and handling it appropriately. * * @param {Arguments} args * @returns {{ error: ?Error, url: ?string, data: *, config: Config, callback: ?function }} */ function normalizeArgs (args) { var error, url, data, config, callback; try { args = Array.prototype.slice.call(args); if (typeof args[args.length - 1] === 'function') { // The last parameter is a callback function callback = args.pop(); } // If the first parameter is a string, then it could be a URL or a JSON/YAML string if (typeof args[0] === 'string' && args[0].trim()[0] !== '{' && args[0].indexOf('\n') === -1) { // The first parameter is the URL url = args[0]; args.shift(); } else { url = ''; } if (typeof args[0] === 'string' || args.length === 2) { // The next parameter is the JSON Schema (as a JSON/YAML string, an object, null, undefined, etc.) data = args[0]; args.shift(); } // The next argument is the config. If null/undefined, then the default config will be used instead config = new Config(args[0]); args.shift(); // There shouldn't be any more arguments left if (args.length > 0) { throw ono('Too many arguments. Expected a URL, schema, config, and optional callback.'); } // Either a url or data is required if (!url && !data) { throw ono('Invalid arguments. Expected at least a URL or schema.'); } } catch (e) { error = e; } return { error: error, url: url, data: data, config: config, callback: callback }; } },{"../Config":1,"ono":37}],6:[function(require,module,exports){ 'use strict'; var Schema = require('../Schema'); var File = require('../File'); var safeCall = require('../../util/safeCall'); var stripHash = require('../../util/stripHash'); var __internal = require('../../util/internal'); var resolveFileReferences = require('./resolveFileReferences'); var STATE_READING = 1; var STATE_READ = 2; module.exports = read; /** * Reads the given file, URL, or data, including any other files or URLs that are referneced * by JSON References ($ref). * * @param {string} url * @param {object|string|undefined} data * @param {Config} config * @param {function} callback * * @this JsonSchemaLib */ function read (url, data, config, callback) { // Create a new JSON Schema and root file var schema = new Schema(config, this.plugins); var rootFile = new File(schema); schema.files.push(rootFile); if (url) { // Resolve the user-supplied URL to an absolute URL url = schema.plugins.resolveURL({ to: url }); // Remove any hash from the URL, since this URL represents the WHOLE file, not a fragment of it rootFile.url = stripHash(url); } if (data) { // No need to read the file, because its data was passed-in rootFile.data = data; rootFile[__internal].state = STATE_READ; safeCall(parseFile, rootFile, callback); } else { // Read/download the file safeCall(readFile, rootFile, callback); } } /** * Reads the given file from its source (e.g. web server, filesystem, etc.) * * @param {File} file * @param {function} callback */ function readFile (file, callback) { var schema = file.schema; file[__internal].state = STATE_READING; if (schema.config.sync) { schema.plugins.readFileSync({ file: file }); doneReading(null); } else { schema.plugins.readFileAsync({ file: file }, doneReading); } function doneReading (err) { if (err) { callback(err); } else { file[__internal].state = STATE_READ; safeCall(decodeFile, file, callback); } } } /** * Decodes the {@link File#data} property of the given file. * * @param {File} file * @param {function} callback */ function decodeFile (file, callback) { file.schema.plugins.decodeFile({ file: file }); safeCall(parseFile, file, callback); } /** * Parses the {@link File#data} property of the given file. * * @param {File} file * @param {function} callback */ function parseFile (file, callback) { file.schema.plugins.parseFile({ file: file }); // Find all JSON References ($ref) to other files, and add new File objects to the schema resolveFileReferences(file); safeCall(readReferencedFiles, file.schema, callback); } /** * Reads any files in the schema that haven't been read yet. * * @param {Schema} schema * @param {function} callback */ function readReferencedFiles (schema, callback) { var filesBeingRead = [], filesToRead = []; var file, i; // Check the state of all files in the schema for (i = 0; i < schema.files.length; i++) { file = schema.files[i]; if (file[__internal].state < STATE_READING) { filesToRead.push(file); } else if (file[__internal].state < STATE_READ) { filesBeingRead.push(file); } } // Have we finished reading everything? if (filesToRead.length === 0 && filesBeingRead.length === 0) { return safeCall(finished, schema, callback); } // In sync mode, just read the next file. // In async mode, start reading all files in the queue var numberOfFilesToRead = schema.config.sync ? 1 : filesToRead.length; for (i = 0; i < numberOfFilesToRead; i++) { file = filesToRead[i]; safeCall(readFile, file, callback); } } /** * Performs final cleanup steps on the schema after all files have been read successfully. * * @param {Schema} schema * @param {function} callback */ function finished (schema, callback) { schema.plugins.finished(); delete schema.config.sync; callback(null, schema); } },{"../../util/internal":27,"../../util/safeCall":31,"../../util/stripHash":33,"../File":2,"../Schema":15,"./resolveFileReferences":7}],7:[function(require,module,exports){ 'use strict'; var File = require('../File'); var typeOf = require('../../util/typeOf'); var stripHash = require('../../util/stripHash'); module.exports = resolveFileReferences; /** * Resolves all JSON References ($ref) to other files, and adds new {@link File} objects * to the schema as needed. * * @param {File} file - The file to search for JSON References */ function resolveFileReferences (file) { // Start crawling at the root of the file crawl(file.data, file); } /** * Recursively crawls the given value, and resolves any external JSON References. * * @param {*} obj - The value to crawl. If it's not an object or array, it will be ignored. * @param {File} file - The file that the value is part of */ function crawl (obj, file) { var type = typeOf(obj); if (!type.isPOJO && !type.isArray) { return; } if (type.isPOJO && isFileReference(obj)) { // We found a file reference, so resolve it resolveFileReference(obj.$ref, file); } // Crawl this POJO or Array, looking for nested JSON References // // NOTE: According to the spec, JSON References should not have any properties other than "$ref". // However, in practice, many schema authors DO add additional properties. Because of this, // we crawl JSON Reference objects just like normal POJOs. If the schema author has added // additional properties, then they have opted-into this non-spec-compliant behavior. var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { var key = keys[i]; var value = obj[key]; crawl(value, file); } } /** * Determines whether the given value is a JSON Reference that points to a file * (as opposed to an internal reference, which points to a location within its own file). * * @param {*} value - The value to inspect * @returns {boolean} */ function isFileReference (value) { return typeof value.$ref === 'string' && value.$ref[0] !== '#'; } /** * Resolves the given JSON Reference URL against the specified file, and adds a new {@link File} * object to the schema if necessary. * * @param {string} url - The JSON Reference URL (may be absolute or relative) * @param {File} file - The file that the JSON Reference is in */ function resolveFileReference (url, file) { var schema = file.schema; var newFile = new File(schema); // Remove any hash from the URL, since this URL represents the WHOLE file, not a fragment of it url = stripHash(url); // Resolve the new file's absolute URL newFile.url = schema.plugins.resolveURL({ from: file.url, to: url }); // Add this file to the schema, unless it already exists if (!schema.files.exists(newFile)) { schema.files.push(newFile); } } },{"../../util/stripHash":33,"../../util/typeOf":34,"../File":2}],8:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var assign = require('../../util/assign'); var __internal = require('../../util/internal'); var validatePlugins = require('./validatePlugins'); var callSyncPlugin = require('./callSyncPlugin'); var callAsyncPlugin = require('./callAsyncPlugin'); module.exports = PluginHelper; /** * Helper methods for working with plugins. * * @param {object[]|null} plugins - The plugins to use * @param {Schema} schema - The {@link Schema} to apply the plugins to * * @class * @extends Array */ function PluginHelper (plugins, schema) { validatePlugins(plugins); plugins = plugins || []; // Clone the array of plugins, and sort by priority var pluginHelper = plugins.slice().sort(sortByPriority); /** * Internal stuff. Use at your own risk! * * @private */ pluginHelper[__internal] = { /** * A reference to the {@link Schema} object */ schema: schema, }; // Return an array that "inherits" from PluginHelper return assign(pluginHelper, PluginHelper.prototype); } /** * Resolves a URL, relative to a base URL. * * @param {?string} args.from - The base URL to resolve against, if any * @param {string} args.to - The URL to resolve. This may be absolute or relative. * @returns {string} - Returns an absolute URL */ PluginHelper.prototype.resolveURL = function resolveURL (args) { try { var handled = callSyncPlugin(this, 'resolveURL', args); var url = handled.result; var plugin = handled.plugin || { name: '' }; if (url === undefined || url === null) { throw ono('Error in %s.resolveURL: No value was returned', plugin.name); } else if (typeof url !== 'string') { throw ono('Error in %s.resolveURL: The return value was not a string (%s)', plugin.name, typeof url); } else { return url; } } catch (err) { throw ono(err, 'Unable to resolve %s', args.to); } }; /** * Synchronously reads the given file from its source (e.g. web server, filesystem, etc.) * * @param {File} args.file - The {@link File} to read */ PluginHelper.prototype.readFileSync = function readFileSync (args) { try { var handled = callSyncPlugin(this, 'readFileSync', args); if (!handled.plugin) { throw ono('Error in readFileSync: No plugin was able to read the file'); } else { // The file was read successfully, so set the file's data args.file.data = handled.result; } } catch (err) { throw ono(err, 'Unable to read %s', args.file.url); } }; /** * Asynchronously reads the given file from its source (e.g. web server, filesystem, etc.) * * @param {File} args.file * The {@link File} to read. Its {@link File#data} property will be set to the file's contents. * In addition, {@link File#mimeType} and {@link File#encoding} may be set, if determinable. * * @param {function} callback * The callback function to call after the file has been read */ PluginHelper.prototype.readFileAsync = function readFileAsync (args, callback) { callAsyncPlugin(this, 'readFileAsync', args, function (err, handled) { if (!err && !handled.plugin) { err = ono('Error in readFileAsync: No plugin was able to read the file'); } if (err) { err = ono(err, 'Unable to read %s', args.file.url); callback(err); } else { if (handled.plugin) { // The file was read successfully, so set the file's data args.file.data = handled.result; } callback(null); } }); }; /** * Decodes the given file's data, in place. * * @param {File} args.file - The {@link File} to decode. */ PluginHelper.prototype.decodeFile = function decodeFile (args) { try { var handled = callSyncPlugin(this, 'decodeFile', args); // NOTE: It's ok if no plugin handles this method. // The file data will just remain in its "raw" format. if (handled.plugin) { // The file was decoded successfully, so update the file's data args.file.data = handled.result; } } catch (err) { throw ono(err, 'Unable to parse %s', args.file.url); } }; /** * Parses the given file's data, in place. * * @param {File} args.file - The {@link File} to parse. */ PluginHelper.prototype.parseFile = function parseFile (args) { try { var handled = callSyncPlugin(this, 'parseFile', args); // NOTE: It's ok if no plugin handles this method. // The file data will just remain in its "raw" format. if (handled.plugin) { // The file was parsed successfully, so update the file's data args.file.data = handled.result; } } catch (err) { throw ono(err, 'Unable to parse %s', args.file.url); } }; /** * Performs final cleanup steps on the schema after all files have been read successfully. */ PluginHelper.prototype.finished = function finished () { try { // NOTE: It's ok if no plugin handles this method. // It's just an opportunity for plugins to perform cleanup tasks if necessary. callSyncPlugin(this, 'finished', {}); } catch (err) { throw ono(err, 'Error finalizing schema'); } }; /** * Used to sort plugins by priority, so that plugins with higher piority come first * in the __plugins array. * * @param {object} pluginA * @param {object} pluginB * @returns {number} */ function sortByPriority (pluginA, pluginB) { return (pluginB.priority || 0) - (pluginA.priority || 0); } },{"../../util/assign":25,"../../util/internal":27,"./callAsyncPlugin":9,"./callSyncPlugin":10,"./validatePlugins":13,"ono":37}],9:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var __internal = require('../../util/internal'); var safeCall = require('../../util/safeCall'); var filterByMethod = require('./filterByMethod'); module.exports = callAsyncPlugin; /** * Calls an asynchronous plugin method with the given arguments. * * @param {PluginHelper} pluginHelper - The {@link PluginHelper} whose plugins are called * @param {string} methodName - The name of the plugin method to call * @param {object} args - The arguments to pass to the method * @param {function} callback - The callback to call when the method finishes */ function callAsyncPlugin (pluginHelper, methodName, args, callback) { var plugins = pluginHelper.filter(filterByMethod(methodName)); args.schema = pluginHelper[__internal].schema; args.config = args.schema.config; safeCall(callNextPlugin, plugins, methodName, args, callback); } /** * Calls the the next plugin from an array of plugins. * * @param {object[]} plugins - The array of plugins * @param {string} methodName - The name of the plugin method to call * @param {object} args - The arguments to pass to the method * @param {function} callback - The callback to call if the plugin returns a result or throws an error */ function callNextPlugin (plugins, methodName, args, callback) { var nextCalled; var plugin = plugins.shift(); var Promise = args.config.Promise; if (!plugin) { // We've reached the end of the plugin chain. No plugin returned a value. callback(null, { plugin: null, result: undefined }); } // Invoke the plugin method. It can return a value, return a Promise, throw an error, or call next() args.next = next; var returnValue = plugin[methodName].call(null, args); if (returnValue !== undefined) { Promise.resolve(returnValue).then(function (result) { var err; if (nextCalled) { err = ono('Error in %s.%s: Cannot return a value and call next()', plugin.name, methodName); } done(err, result); }); } function next (err, result) { if (nextCalled) { err = ono('Error in %s.%s: next() was called multiple times', plugin.name, methodName); } nextCalled = true; done(err, result); } function done (err, result) { if (err) { callback(ono(err, 'Error in %s.%s:', plugin.name, methodName)); } else if (nextCalled && result === undefined) { safeCall(callNextPlugin, plugins, methodName, args, callback); } else { // next() was NOT called, so return the plugin's result (even if there was no return value) callback(null, { plugin: plugin, result: result }); } } } },{"../../util/internal":27,"../../util/safeCall":31,"./filterByMethod":11,"ono":37}],10:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var __internal = require('../../util/internal'); var filterByMethod = require('./filterByMethod'); module.exports = callSyncPlugin; /** * Calls a synchronous plugin method with the given arguments. * * @param {PluginHelper} pluginHelper - The {@link PluginHelper} whose plugins are called * @param {string} methodName - The name of the plugin method to call * @param {object} args - The arguments to pass to the method * * @returns {{ result: *, plugin: ?object }} * If the method was handled by a plugin (i.e. the plugin didn't call next()), then the returned * object will contain a reference to the plugin, and the result that was returned by the plugin. */ function callSyncPlugin (pluginHelper, methodName, args) { var plugins = pluginHelper.filter(filterByMethod(methodName)); args.schema = pluginHelper[__internal].schema; args.config = args.schema.config; return callNextPlugin(plugins, methodName, args); } /** * Calls the the next plugin from an array of plugins. * * @param {object[]} plugins - The array of plugins * @param {string} methodName - The name of the plugin method to call * @param {object} args - The arguments to pass to the method * @returns {{ plugin: ?object, result: * }} */ function callNextPlugin (plugins, methodName, args) { var result, error, nextCalled; var plugin = plugins.shift(); if (!plugin) { // We've reached the end of the plugin chain. No plugin returned a value. return { plugin: null, result: undefined }; } // Invoke the plugin method. It can return a value, throw an error, or call next() args.next = next; result = plugin[methodName].call(null, args); if (result !== undefined && nextCalled) { throw ono('Error in %s.%s: Cannot return a value and call next()', plugin.name, methodName); } if (error) { throw ono(error, 'Error in %s.%s:', plugin.name, methodName); } else if (nextCalled && result === undefined) { return callNextPlugin(plugins, methodName, args); } else { // next() was NOT called, so return the plugin's result (even if there was no return value) return { plugin: plugin, result: result }; } function next (err, value) { if (nextCalled) { error = ono('Error in %s.%s: next() was called multiple times', plugin.name, methodName); } nextCalled = true; error = err; result = value; } } },{"../../util/internal":27,"./filterByMethod":11,"ono":37}],11:[function(require,module,exports){ 'use strict'; module.exports = filterByMethod; /** * Used to filter plugins that implement the specified method. * * @param {string} methodName * @returns {function} */ function filterByMethod (methodName) { return function methodFilter (plugin) { return typeof plugin[methodName] === 'function'; }; } },{}],12:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var typeOf = require('../../util/typeOf'); module.exports = validatePlugin; /** * Ensures that a user-supplied value is a valid plugin POJO. * An error is thrown if the value is invalid. * * @param {*} plugin - The user-supplied value to validate */ function validatePlugin (plugin) { var type = typeOf(plugin); if (!type.isPOJO) { throw ono('Invalid arguments. Expected a plugin object.'); } } },{"../../util/typeOf":34,"ono":37}],13:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var typeOf = require('../../util/typeOf'); var validatePlugin = require('./validatePlugin'); module.exports = validatePlugins; /** * Ensures that a user-supplied value is a valid array of plugins. * An error is thrown if the value is invalid. * * @param {*} plugins - The user-supplied value to validate */ function validatePlugins (plugins) { var type = typeOf(plugins); if (type.hasValue) { if (type.isArray) { // Make sure all the items in the array are valid plugins plugins.forEach(validatePlugin); } else { throw ono('Invalid arguments. Expected an array of plugins.'); } } } },{"../../util/typeOf":34,"./validatePlugin":12,"ono":37}],14:[function(require,module,exports){ 'use strict'; var ono = require('ono'); var typeOf = require('../util/typeOf'); var assign = require('../util/assign'); var deepAssign = require('../util/deepAssign'); var validatePlugin = require('./PluginHelper/validatePlugin'); var validatePlugins = require('./PluginHelper/validatePlugins'); module.exports = PluginManager; /** * Manages the plugins that are used by a {@link JsonSchemaLib} instance. * * @param {object[]} [plugins] - The initial plugins to load * * @class * @extends Array */ function PluginManager (plugins) { validatePlugins(plugins); plugins = plugins || PluginManager.defaults; // Clone the plugins, so that multiple JsonSchemaLib instances can safely use the same plugins var pluginManager = plugins.map(clonePlugin); // Return an array that "inherits" from PluginManager return assign(pluginManager, PluginManager.prototype); } /** * The default plugins that are used if no plugins are specified. * * NOTE: The default plugins differ for Node.js and web browsers. * * @type {object[]} */ PluginManager.defaults = []; /** * Adds a plugin to this {@link PluginManager} instance. * * @param {object} plugin - A plugin object * @param {number} [priority] - Optionaly override the plugin's default priority. * @private */ PluginManager.prototype.use = function use (plugin, priority) { validatePlugin(plugin); validatePriority(priority); // Clone the plugin, so that multiple JsonSchemaLib instances can safely use the same plugin plugin = clonePlugin(plugin); plugin.priority = priority || plugin.priority; this.push(plugin); }; /** * Ensures that a user-supplied value is a valid plugin priority. * An error is thrown if the value is invalid. * * @param {*} priority - The user-supplied value to validate */ function validatePriority (priority) { var type = typeOf(priority); if (type.hasValue && !type.isNumber) { throw ono('Invalid arguments. Expected a priority number.'); } } /** * Returns a deep clone of the given plugin. * * @param {object} plugin * @returns {object} */ function clonePlugin (plugin) { var clone = {}; return deepAssign(clone, plugin); } },{"../util/assign":25,"../util/deepAssign":26,"../util/typeOf":34,"./PluginHelper/validatePlugin":12,"./PluginHelper/validatePlugins":13,"ono":37}],15:[function(require,module,exports){ 'use strict'; var Config = require('./Config'); var PluginHelper = require('./PluginHelper/PluginHelper'); var FileArray = require('./FileArray'); module.exports = Schema; /** * This class represents the entire JSON Schema. It contains information about all the files in the * schema, and provides methods to traverse the schema and get/set values within it using * JSON Pointers. * * @param {Config|object} [config] - The config settings that apply to the schema * @param {object[]} [plugins] - The plugins to use for the schema * * @class */ function Schema (config, plugins) { /** * The config settings that apply to this schema. * * @type {Config} */ this.config = new Config(config); /** * The plugins to use for this schema. * * @type {PluginHelper} */ this.plugins = new PluginHelper(plugins, this); /** * All of the files in the schema, including the main schema file itself * * @type {File[]} * @readonly */ this.files = new FileArray(this); } Object.defineProperties(Schema.prototype, { /** * The parsed JSON Schema. * * @type {object|null} */ root: { configurable: true, enumerable: true, get: function () { if (this.files.length === 0) { return null; } return this.files[0].data; } }, /** * The URL of the main JSON Schema file. * * @type {string|null} */ rootURL: { configurable: true, enumerable: true, get: function () { if (this.files.length === 0) { return null; } return this.files[0].url; } }, /** * The main JSON Schema file. * * @type {File} */ rootFile: { configurable: true, enumerable: true, get: function () { return this.files[0] || null; } }, }); /** * Returns a human-friendly representation of the Schema object. * * @returns {string} */ Schema.prototype.toString = function () { var rootFile = this.rootFile; if (rootFile) { return rootFile.toString(); } else { return '(empty JSON schema)'; } }; /** * Determines whether a given value exists in the schema. * * @param {string} pointer - A JSON Pointer that points to the value to check. * Or a URL with a url-encoded JSON Pointer in the hash. * * @returns {boolean} - Returns true if the value exists, or false otherwise */ Schema.prototype.exists = function (pointer) { // eslint-disable-line no-unused-vars // TODO: pointer can be a JSON Pointer (starting with a /) or a URL }; /** * Finds a value in the schema. * * @param {string} pointer - A JSON Pointer that points to the value to get. * Or a URL with a url-encoded JSON Pointer in the hash. * * @returns {*} - Returns the specified value, which can be ANY JavaScript type, including * an object, array, string, number, null, undefined, NaN, etc. * If the value is not found, then an error is thrown. */ Schema.prototype.get = function (pointer) { // eslint-disable-line no-unused-vars // TODO: pointer can be a JSON Pointer (starting with a /) or a URL }; /** * Sets a value in the schema. * * @param {string} pointer - A JSON Pointer that points to the value to set. * Or a URL with a url-encoded JSON Pointer in the hash. * * @param {*} value - The value to assign. This can be ANY JavaScript type, including * an object, array, string, number, null, undefined, NaN, etc. */ Schema.prototype.set = function (pointer, value) { // eslint-disable-line no-unused-vars // TODO: pointer can be a JSON Pointer (starting with a /) or a URL }; },{"./Config":1,"./FileArray":3,"./PluginHelper/PluginHelper":8}],16:[function(require,module,exports){ 'use strict'; var PluginManager = require('./api/PluginManager'); // Default plugins for web browsers PluginManager.defaults.push( require('./plugins/BrowserUrlPlugin'), require('./plugins/XMLHttpRequestPlugin'), require('./plugins/TextDecoderPlugin'), require('./plugins/ArrayDecoderPlugin'), require('./plugins/JsonPlugin') ); module.exports = require('./exports'); },{"./api/PluginManager":14,"./exports":17,"./plugins/ArrayDecoderPlugin":18,"./plugins/BrowserUrlPlugin":19,"./plugins/JsonPlugin":20,"./plugins/TextDecoderPlugin":21,"./plugins/XMLHttpRequestPlugin":22}],17:[function(require,module,exports){ 'use strict'; var JsonSchemaLib = require('./api/JsonSchemaLib/JsonSchemaLib'); var Schema = require('./api/Schema'); var File = require('./api/File'); /** * The default instance of {@link JsonSchemaLib} * * @type {JsonSchemaLib} */ module.exports = createJsonSchemaLib(); // Bind the "read" methods of the default instance, so they can be used as standalone functions module.exports.read = JsonSchemaLib.prototype.read.bind(module.exports); module.exports.readAsync = JsonSchemaLib.prototype.readAsync.bind(module.exports); module.exports.readSync = JsonSchemaLib.prototype.readSync.bind(module.exports); /** * Allows ES6 default import syntax (for Babel, TypeScript, etc.) * * @type {JsonSchemaLib} */ module.exports.default = module.exports; /** * Factory function for creating new instances of {@link JsonSchemaLib} */ module.exports.create = createJsonSchemaLib; /** * Utility methods for plugin developers */ module.exports.util = { /** * Determines whether the given value is a {@link Schema} object * * @param {*} value * @returns {boolean} */ isSchema: function isSchema (value) { return value instanceof Schema; }, /** * Determines whether the given value is a {@link File} object * * @param {*} value * @returns {boolean} */ isFile: function isFile (value) { return value instanceof File; }, }; /** * Creates an instance of JsonSchemaLib * * @param {Config} [config] - The configuration to use. Can be overridden by {@link JsonSchemaLib#read} * @param {object[]} [plugins] - The plugins to use. Additional plugins can be added via {@link JsonSchemaLib#use} * @returns {JsonSchemaLib} */ function createJsonSchemaLib (config, plugins) { return new JsonSchemaLib(config, plugins); } },{"./api/File":2,"./api/JsonSchemaLib/JsonSchemaLib":4,"./api/Schema":15}],18:[function(require,module,exports){ 'use strict'; var isTypedArray = require('../util/isTypedArray'); /** * This plugin decodes arrays of bytes (such as TypedArrays or ArrayBuffers) to strings, if possible. */ module.exports = { name: 'ArrayDecoderPlugin', /** * This plugin has a lower priority than the BufferDecoderPlugin or TextDecoderPlugin, so it will only be used * as a final fallback if neither of the other decoders is able to decode the file's data. */ priority: 5, /** * Decodes the given file's data, in place. * * @param {File} args.file - The {@link File} to decode. * @param {function} args.next - Calls the next plugin, if the file data cannot be decoded * @returns {string|undefined} */ decodeFile: function decodeFile (args) { var file = args.file; var next = args.next; if (file.encoding && (isTypedArray(file.data) || Array.isArray(file.data))) { try { // Normalize the data to 2-byte characters var characterArray = new Uint16Array(file.data); // Convert the characters to a string var string = String.fromCharCode.apply(null, characterArray); // Remove the byte order mark, if any return stripBOM(string); } catch (err) { // Unknown encoding, so just call the next decoder plugin next(); } } else { // The file data is not a supported data type, so call the next decoder plugin next(); } }, }; /** * Removes the UTF-16 byte order mark, if any, from a string. * * @param {string} str * @returns {string} */ function stripBOM (str) { var bom = str.charCodeAt(0); // Check for the UTF-16 byte order mark (0xFEFF or 0xFFFE) if (bom === 0xFEFF || bom === 0xFFFE) { return str.slice(1); } return str; } },{"../util/isTypedArray":28}],19:[function(require,module,exports){ 'use strict'; var stripHash = require('../util/stripHash'); // Matches any RFC 3986 URL with a scheme (e.g. "http://", "ftp://", "file://") var protocolPattern = /^[a-z][a-z\d\+\-\.]*:\/\//i; /** * This plugin resolves URLs using the WHATWG URL API, if supported by the current browser. * Relative URLs are resolved against the browser's curreng page URL. */ module.exports = { name: 'BrowserUrlPlugin', /** * This plugin's priority is the same as the NodeUrlPlugin's priority, for consistency between the * Node.js and web browser functionality. */ priority: 20, /** * Resolves a URL, relative to a base URL. * * @param {?string} args.from * The base URL to resolve against. If unset, then the current page URL is used. * * @param {string} args.to * The URL to resolve. This may be absolute or relative. If relative, then it will be resolved * against {@link args.from} * * @param {function} args.next * Calls the next plugin, if the URL is not an HTTP or HTTPS URL. * * @returns {string|undefined} */ resolveURL: function resolveURL (args) { var from = args.from; var to = args.to; if (typeof URL === 'function') { // This browser supports the WHATWG URL API return new URL(to, from || location.href).href; } else if (protocolPattern.test(to)) { // It's an absolute URL, so return it as-is return to; } else if (to.substr(0, 2) === '//') { return resolveProtocolRelativeURL(from, to); } else if (to[0] === '/') { return resolveOriginRelativeURL(from, to); } else { return resolvePathRelativeURL(from, to); } }, }; /** * Resolves a protocol-relative URL, such as "//example.com/directory/file.json". * * @param {?string} absolute - The absolute URL to resolve against. Defaults to the current page URL. * @param {string} relative - The relative URL to resolve * @returns {string} */ function resolveProtocolRelativeURL (absolute, relative) { var protocol; if (absolute) { // Get the protocol from the absolute URL protocol = protocolPattern.exec(absolute)[0]; } else { // Use the current page's protocol protocol = location.protocol; } return protocol + relative; } /** * Resolves an origin-relative URL, such as "/file.json", "/dir/subdir/file.json", etc. * * @param {?string} absolute - The absolute URL to resolve against. Defaults to the current page URL. * @param {string} relative - The relative URL to resolve * @returns {string} */ function resolveOriginRelativeURL (absolute, relative) { var origin; if (absolute) { // Get the origin from the absolute URL by joining the first 3 segments (e.g. "http", "", "example.com") origin = absolute.split('/').splice(0, 3).join('/'); } else { // Use the current page's origin origin = location.origin || (location.protocol + location.host); } return origin + relative; } /** * Resolves a path-relative URL, such as "file.json", "../file.json", "../dir/subdir/file.json", etc. * * @param {?string} absolute - The absolute URL to resolve against. Defaults to the current page URL. * @param {string} relative - The relative URL to resolve * @returns {string} */ function resolvePathRelativeURL (absolute, relative) { // If there's no absolute URL, then use the current page URL (without query or hash) if (!absolute) { absolute = stripHash(stripQuery(location.href)); } var absoluteSegments = absolute.split('/'); var relativeSegments = relative.split('/'); // The first 3 segments of the absolute URL are the origin (e.g. "http://www.example.com") var origin = absoluteSegments.splice(0, 3).join('/'); // Remove the file name from the absolute URL, so it's just a directory absoluteSegments.pop(); // Add each segment of the relative URL to the absolute URL, accounting for "." and ".." segments for (var i = 0; i < relativeSegments.length; i++) { var segment = relativeSegments[i]; switch (segment) { case '.': break; case '..': absoluteSegments.pop(); break; default: absoluteSegments.push(segment); } } return origin + '/' + absoluteSegments.join('/'); } function stripQuery (url) { var queryIndex = url.indexOf('?'); if (queryIndex >= 0) { url = url.substr(0, queryIndex); } return url; } },{"../util/stripHash":33}],20:[function(require,module,exports){ 'use strict'; // Matches "application/json", "text/json", "application/hal+json", etc. var mimeTypePattern = /[/+]json$/; // Matches any URL that ends with ".json" (ignoring the query and hash) var extensionPattern = /^[^\?\#]+\.json(\?.*)?$/; /** * This plugin parses JSON files */ module.exports = { name: 'JsonPlugin', /** * This plugin has a low priority, to allow for higher-priority third-party parser plugins. * * NOTE: Priorities 0 - 99 are reserved for JsonSchemaLib. * Third-party plugins should have priorities of 100 or greater. */ priority: 20, /** * Parses the given file's data, in place. * * @param {File} args.file - The {@link File} to parse. * @param {function} args.next - Calls the next plugin, if the file data cannot be parsed * @return