UNPKG

pimatic

Version:

A home automation server and framework for the Raspberry PI running on node.js

300 lines (255 loc) 10.6 kB
// // JaySchema (draft v4) validator for Node.js. // 'use strict'; var Errors = require('./errors.js') , crypto = require('crypto') , SchemaRegistry = require('./schemaRegistry.js') , when = require('when') , keys = require('when/keys') ; var DEFAULT_SCHEMA_VERSION = 'http://json-schema.org/draft-04/schema#'; var ANON_URI_SCHEME = 'anon-schema'; var testRunners = { 'http://json-schema.org/draft-04/schema#': require('./suites/draft-04'), 'http://json-schema.org/schema#': require('./suites/draft-04') }; // ****************************************************************** // Constructor // ****************************************************************** var JaySchema = function(loader) { // public this.maxRecursion = 5; this.loader = (typeof loader === 'function') ? loader : null; // internal this._schemaRegistry = new SchemaRegistry(); this._customFormatHandlers = {}; // _refsRequested is an object where the key is the normalized ID // of the schema ref that was requested, and the value is a // promise for the content of that schema. this._refsRequested = {}; }; // ****************************************************************** // [static] Pre-defined schema loaders (can be passed to the // constructor) // ****************************************************************** JaySchema.loaders = { http: require('./httpLoader.js') }; // ****************************************************************** // [static] Export the errors classes so they can be used by schema // loaders and others. // ****************************************************************** JaySchema.errors = Errors; // ****************************************************************** // Get an array of $refs for which we don’t have a schema yet. // ****************************************************************** JaySchema.prototype.getMissingSchemas = function() { return this._schemaRegistry.getMissingSchemas(); }; // ****************************************************************** // Return boolean indicating whether the specified schema id has // previously been registered. // ****************************************************************** JaySchema.prototype.isRegistered = function(id) { return this._schemaRegistry.isRegistered(id); }; // ****************************************************************** // Register a schema. // ****************************************************************** JaySchema.prototype.register = function() { return this._schemaRegistry.register.apply(this._schemaRegistry, arguments); }; // ****************************************************************** // Register a handler for the format keyword. Handlers have the // following signature: // // function handler(value) -> returns null (if value is valid) or // error description string (if value is // not valid) // ****************************************************************** JaySchema.prototype.addFormat = function(attributeName, handler) { // Wrap the format handler to make its signature match that of the // internal handlers. var handlerWrapper = function(config) { var errors = []; var result = handler(config.inst, config.schema); if (typeof result === 'string') { errors.push(new Errors.FormatValidationError(config.resolutionScope, config.instanceContext, 'format', attributeName, config.inst, result)); } else if (result !== null) { var desc = 'failed custom format validation'; errors.push(new Errors.FormatValidationError(config.resolutionScope, config.instanceContext, 'format', attributeName, config.inst, desc)); } return errors; }; this._customFormatHandlers[attributeName] = handlerWrapper; }; // ****************************************************************** // [static] Return a hash for an object. We rely on JSON.stringify // to always return the same value for a given object. (If it // doesn’t return the same value, the parser will be somewhat slower // and use more memory. It does seem to always return the same // value based on observation and on Ecma-262 5.1 § 15.12.3.) // ****************************************************************** JaySchema._getObjectHash = function(obj) { var shasum = crypto.createHash('sha1'); shasum.update(JSON.stringify(obj)); return shasum.digest('hex'); }; // ****************************************************************** // Helper to call the user-provided schema loader. // ****************************************************************** JaySchema.prototype._loadMissingRefs = function(depth, callback) { var err; // try not to request the same ref more than once var schemasNeeded = this._schemaRegistry.getMissingSchemas(); // get a list of missing refs that have not been requested yet var missing = schemasNeeded.filter(function(ref) { var registered = this._schemaRegistry.isRegistered(ref); var requested = (SchemaRegistry._normalizeId(ref) in this._refsRequested); return (!registered && !requested); }, this); // are we in too deep? if (missing.length && !depth) { var desc = 'would exceed max recursion depth fetching these referenced ' + 'schemas (set the maxRecursion property if you need to go deeper): ' + missing; err = new Errors.ValidationError(undefined, undefined, undefined, undefined, undefined, desc); return callback(err); } // function called when the loader finishes loading a schema var onLoaded = function(ref, deferred, loaderErr, schema) { if (loaderErr) { return deferred.reject(loaderErr); } if (!this._schemaRegistry.isRegistered(ref)) { this.register(schema, ref); } deferred.resolve(schema); }; // now request the missing refs that have not been requested yet for (var index = 0, len = missing.length; index !== len; ++index) { var ref = missing[index]; var deferred = when.defer(); this._refsRequested[SchemaRegistry._normalizeId(ref)] = deferred.promise; this.loader(ref, onLoaded.bind(this, ref, deferred)); } // wait for all of them var self = this; keys.all(this._refsRequested).then( function() { // all promises fulfilled if (missing.length) { // We loaded some schemas. We need to recurse to load // additional schemas that were referenced by the schemas we // just loaded. self._loadMissingRefs(depth - 1, callback); } else { // We didn’t load any more schemas ourselves; we were just // waiting for any loader promises to be fulfilled. callback(); } }, callback // one or more promises rejected ); }; // ****************************************************************** // The main validation guts (internal implementation). // ****************************************************************** JaySchema.prototype._validateImpl = function(instance, schema, resolutionScope, instanceContext) { // for schemas that have no id, use an internal anonymous id var schemaId = schema.id || resolutionScope || ANON_URI_SCHEME + '://' + JaySchema._getObjectHash(schema) + '/#'; if (!this._schemaRegistry.isRegistered(schemaId)) { this.register(schema, schemaId, resolutionScope); } resolutionScope = resolutionScope || schemaId; if (resolutionScope.indexOf('#') === -1) { resolutionScope += '#'; } // no schema passed if (!schema) { return [];} // run the tests var config = { clone: function() { var result = {}; var keys = Object.keys(this); for (var index = 0, len = keys.length; index !== len; ++index) { result[keys[index]] = this[keys[index]]; } return result; }, inst: instance, schema: schema, resolutionScope: resolutionScope, instanceContext: instanceContext || '#', schemaRegistry: this._schemaRegistry, customFormatHandlers: this._customFormatHandlers }; var testRunner = testRunners[schema.$schema || DEFAULT_SCHEMA_VERSION]; if (!testRunner) { var desc = 'The top-level schema (' + schema.$schema + ') is not ' + 'supported by this validator. Supported schemas are: ' + Object.keys(testRunners).join(', '); var err = new Errors.ValidationError(undefined, undefined, undefined, undefined, undefined, desc); return [err]; } return testRunner(config); }; // ****************************************************************** // The main validation function (public API). Our raison d'être. // ****************************************************************** JaySchema.prototype.validate = function(instance, schema, callback) { var desc, err; if (typeof schema === 'string') { schema = this._schemaRegistry.get(schema) || { $ref: schema }; } if (!schema) { desc = 'No schema provided for validation.'; err = new Errors.ValidationError(undefined, '#', undefined, undefined, undefined, desc); if (callback) { return process.nextTick(callback.bind(null, [err])); } else { return [err]; } } // for schemas that have no id, use an internal anonymous id var schemaId = schema.id || ANON_URI_SCHEME + '://' + JaySchema._getObjectHash(schema) + '/#'; this.register(schema, schemaId); if (callback) { var self = this; var result; if (self.loader) { // If the user provided a loader callback, load all unresolved // $references schemas at this time. self._loadMissingRefs(self.maxRecursion, function(err) { if (err) { return callback([err]); } result = self._validateImpl(instance, schema); if (result.length) { process.nextTick(callback.bind(null, result)); } else { process.nextTick(callback); } }); } else { // no loader, but user still wants a callback result = this._validateImpl(instance, schema); if (result.length) { process.nextTick(callback.bind(null, result)); } else { process.nextTick(callback); } } } else { // traditional, non-callback validation var errs = []; if (this.loader) { desc = 'You provided a loader callback, but you are calling ' + 'validate() synchronously. Your loader will be ignored and ' + 'validation will fail if any missing $refs are encountered.'; err = new Errors.LoaderAsyncError(undefined, undefined, undefined, undefined, undefined, desc); errs.push(err); } errs = errs.concat(this._validateImpl(instance, schema)); return errs; } }; module.exports = JaySchema;