388 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			388 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | 'use strict'; | ||
|  | 
 | ||
|  | var resolve = require('./resolve') | ||
|  |   , util = require('./util') | ||
|  |   , errorClasses = require('./error_classes') | ||
|  |   , stableStringify = require('fast-json-stable-stringify'); | ||
|  | 
 | ||
|  | var validateGenerator = require('../dotjs/validate'); | ||
|  | 
 | ||
|  | /** | ||
|  |  * Functions below are used inside compiled validations function | ||
|  |  */ | ||
|  | 
 | ||
|  | var ucs2length = util.ucs2length; | ||
|  | var equal = require('fast-deep-equal'); | ||
|  | 
 | ||
|  | // this error is thrown by async schemas to return validation errors via exception
 | ||
|  | var ValidationError = errorClasses.Validation; | ||
|  | 
 | ||
|  | module.exports = compile; | ||
|  | 
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * Compiles schema to validation function | ||
|  |  * @this   Ajv | ||
|  |  * @param  {Object} schema schema object | ||
|  |  * @param  {Object} root object with information about the root schema for this schema | ||
|  |  * @param  {Object} localRefs the hash of local references inside the schema (created by resolve.id), used for inline resolution | ||
|  |  * @param  {String} baseId base ID for IDs in the schema | ||
|  |  * @return {Function} validation function | ||
|  |  */ | ||
|  | function compile(schema, root, localRefs, baseId) { | ||
|  |   /* jshint validthis: true, evil: true */ | ||
|  |   /* eslint no-shadow: 0 */ | ||
|  |   var self = this | ||
|  |     , opts = this._opts | ||
|  |     , refVal = [ undefined ] | ||
|  |     , refs = {} | ||
|  |     , patterns = [] | ||
|  |     , patternsHash = {} | ||
|  |     , defaults = [] | ||
|  |     , defaultsHash = {} | ||
|  |     , customRules = []; | ||
|  | 
 | ||
|  |   root = root || { schema: schema, refVal: refVal, refs: refs }; | ||
|  | 
 | ||
|  |   var c = checkCompiling.call(this, schema, root, baseId); | ||
|  |   var compilation = this._compilations[c.index]; | ||
|  |   if (c.compiling) return (compilation.callValidate = callValidate); | ||
|  | 
 | ||
|  |   var formats = this._formats; | ||
|  |   var RULES = this.RULES; | ||
|  | 
 | ||
|  |   try { | ||
|  |     var v = localCompile(schema, root, localRefs, baseId); | ||
|  |     compilation.validate = v; | ||
|  |     var cv = compilation.callValidate; | ||
|  |     if (cv) { | ||
|  |       cv.schema = v.schema; | ||
|  |       cv.errors = null; | ||
|  |       cv.refs = v.refs; | ||
|  |       cv.refVal = v.refVal; | ||
|  |       cv.root = v.root; | ||
|  |       cv.$async = v.$async; | ||
|  |       if (opts.sourceCode) cv.source = v.source; | ||
|  |     } | ||
|  |     return v; | ||
|  |   } finally { | ||
|  |     endCompiling.call(this, schema, root, baseId); | ||
|  |   } | ||
|  | 
 | ||
|  |   /* @this   {*} - custom context, see passContext option */ | ||
|  |   function callValidate() { | ||
|  |     /* jshint validthis: true */ | ||
|  |     var validate = compilation.validate; | ||
|  |     var result = validate.apply(this, arguments); | ||
|  |     callValidate.errors = validate.errors; | ||
|  |     return result; | ||
|  |   } | ||
|  | 
 | ||
|  |   function localCompile(_schema, _root, localRefs, baseId) { | ||
|  |     var isRoot = !_root || (_root && _root.schema == _schema); | ||
|  |     if (_root.schema != root.schema) | ||
|  |       return compile.call(self, _schema, _root, localRefs, baseId); | ||
|  | 
 | ||
|  |     var $async = _schema.$async === true; | ||
|  | 
 | ||
|  |     var sourceCode = validateGenerator({ | ||
|  |       isTop: true, | ||
|  |       schema: _schema, | ||
|  |       isRoot: isRoot, | ||
|  |       baseId: baseId, | ||
|  |       root: _root, | ||
|  |       schemaPath: '', | ||
|  |       errSchemaPath: '#', | ||
|  |       errorPath: '""', | ||
|  |       MissingRefError: errorClasses.MissingRef, | ||
|  |       RULES: RULES, | ||
|  |       validate: validateGenerator, | ||
|  |       util: util, | ||
|  |       resolve: resolve, | ||
|  |       resolveRef: resolveRef, | ||
|  |       usePattern: usePattern, | ||
|  |       useDefault: useDefault, | ||
|  |       useCustomRule: useCustomRule, | ||
|  |       opts: opts, | ||
|  |       formats: formats, | ||
|  |       logger: self.logger, | ||
|  |       self: self | ||
|  |     }); | ||
|  | 
 | ||
|  |     sourceCode = vars(refVal, refValCode) + vars(patterns, patternCode) | ||
|  |                    + vars(defaults, defaultCode) + vars(customRules, customRuleCode) | ||
|  |                    + sourceCode; | ||
|  | 
 | ||
|  |     if (opts.processCode) sourceCode = opts.processCode(sourceCode); | ||
|  |     // console.log('\n\n\n *** \n', JSON.stringify(sourceCode));
 | ||
|  |     var validate; | ||
|  |     try { | ||
|  |       var makeValidate = new Function( | ||
|  |         'self', | ||
|  |         'RULES', | ||
|  |         'formats', | ||
|  |         'root', | ||
|  |         'refVal', | ||
|  |         'defaults', | ||
|  |         'customRules', | ||
|  |         'equal', | ||
|  |         'ucs2length', | ||
|  |         'ValidationError', | ||
|  |         sourceCode | ||
|  |       ); | ||
|  | 
 | ||
|  |       validate = makeValidate( | ||
|  |         self, | ||
|  |         RULES, | ||
|  |         formats, | ||
|  |         root, | ||
|  |         refVal, | ||
|  |         defaults, | ||
|  |         customRules, | ||
|  |         equal, | ||
|  |         ucs2length, | ||
|  |         ValidationError | ||
|  |       ); | ||
|  | 
 | ||
|  |       refVal[0] = validate; | ||
|  |     } catch(e) { | ||
|  |       self.logger.error('Error compiling schema, function code:', sourceCode); | ||
|  |       throw e; | ||
|  |     } | ||
|  | 
 | ||
|  |     validate.schema = _schema; | ||
|  |     validate.errors = null; | ||
|  |     validate.refs = refs; | ||
|  |     validate.refVal = refVal; | ||
|  |     validate.root = isRoot ? validate : _root; | ||
|  |     if ($async) validate.$async = true; | ||
|  |     if (opts.sourceCode === true) { | ||
|  |       validate.source = { | ||
|  |         code: sourceCode, | ||
|  |         patterns: patterns, | ||
|  |         defaults: defaults | ||
|  |       }; | ||
|  |     } | ||
|  | 
 | ||
|  |     return validate; | ||
|  |   } | ||
|  | 
 | ||
|  |   function resolveRef(baseId, ref, isRoot) { | ||
|  |     ref = resolve.url(baseId, ref); | ||
|  |     var refIndex = refs[ref]; | ||
|  |     var _refVal, refCode; | ||
|  |     if (refIndex !== undefined) { | ||
|  |       _refVal = refVal[refIndex]; | ||
|  |       refCode = 'refVal[' + refIndex + ']'; | ||
|  |       return resolvedRef(_refVal, refCode); | ||
|  |     } | ||
|  |     if (!isRoot && root.refs) { | ||
|  |       var rootRefId = root.refs[ref]; | ||
|  |       if (rootRefId !== undefined) { | ||
|  |         _refVal = root.refVal[rootRefId]; | ||
|  |         refCode = addLocalRef(ref, _refVal); | ||
|  |         return resolvedRef(_refVal, refCode); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     refCode = addLocalRef(ref); | ||
|  |     var v = resolve.call(self, localCompile, root, ref); | ||
|  |     if (v === undefined) { | ||
|  |       var localSchema = localRefs && localRefs[ref]; | ||
|  |       if (localSchema) { | ||
|  |         v = resolve.inlineRef(localSchema, opts.inlineRefs) | ||
|  |             ? localSchema | ||
|  |             : compile.call(self, localSchema, root, localRefs, baseId); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (v === undefined) { | ||
|  |       removeLocalRef(ref); | ||
|  |     } else { | ||
|  |       replaceLocalRef(ref, v); | ||
|  |       return resolvedRef(v, refCode); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function addLocalRef(ref, v) { | ||
|  |     var refId = refVal.length; | ||
|  |     refVal[refId] = v; | ||
|  |     refs[ref] = refId; | ||
|  |     return 'refVal' + refId; | ||
|  |   } | ||
|  | 
 | ||
|  |   function removeLocalRef(ref) { | ||
|  |     delete refs[ref]; | ||
|  |   } | ||
|  | 
 | ||
|  |   function replaceLocalRef(ref, v) { | ||
|  |     var refId = refs[ref]; | ||
|  |     refVal[refId] = v; | ||
|  |   } | ||
|  | 
 | ||
|  |   function resolvedRef(refVal, code) { | ||
|  |     return typeof refVal == 'object' || typeof refVal == 'boolean' | ||
|  |             ? { code: code, schema: refVal, inline: true } | ||
|  |             : { code: code, $async: refVal && !!refVal.$async }; | ||
|  |   } | ||
|  | 
 | ||
|  |   function usePattern(regexStr) { | ||
|  |     var index = patternsHash[regexStr]; | ||
|  |     if (index === undefined) { | ||
|  |       index = patternsHash[regexStr] = patterns.length; | ||
|  |       patterns[index] = regexStr; | ||
|  |     } | ||
|  |     return 'pattern' + index; | ||
|  |   } | ||
|  | 
 | ||
|  |   function useDefault(value) { | ||
|  |     switch (typeof value) { | ||
|  |       case 'boolean': | ||
|  |       case 'number': | ||
|  |         return '' + value; | ||
|  |       case 'string': | ||
|  |         return util.toQuotedString(value); | ||
|  |       case 'object': | ||
|  |         if (value === null) return 'null'; | ||
|  |         var valueStr = stableStringify(value); | ||
|  |         var index = defaultsHash[valueStr]; | ||
|  |         if (index === undefined) { | ||
|  |           index = defaultsHash[valueStr] = defaults.length; | ||
|  |           defaults[index] = value; | ||
|  |         } | ||
|  |         return 'default' + index; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function useCustomRule(rule, schema, parentSchema, it) { | ||
|  |     if (self._opts.validateSchema !== false) { | ||
|  |       var deps = rule.definition.dependencies; | ||
|  |       if (deps && !deps.every(function(keyword) { | ||
|  |         return Object.prototype.hasOwnProperty.call(parentSchema, keyword); | ||
|  |       })) | ||
|  |         throw new Error('parent schema must have all required keywords: ' + deps.join(',')); | ||
|  | 
 | ||
|  |       var validateSchema = rule.definition.validateSchema; | ||
|  |       if (validateSchema) { | ||
|  |         var valid = validateSchema(schema); | ||
|  |         if (!valid) { | ||
|  |           var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); | ||
|  |           if (self._opts.validateSchema == 'log') self.logger.error(message); | ||
|  |           else throw new Error(message); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     var compile = rule.definition.compile | ||
|  |       , inline = rule.definition.inline | ||
|  |       , macro = rule.definition.macro; | ||
|  | 
 | ||
|  |     var validate; | ||
|  |     if (compile) { | ||
|  |       validate = compile.call(self, schema, parentSchema, it); | ||
|  |     } else if (macro) { | ||
|  |       validate = macro.call(self, schema, parentSchema, it); | ||
|  |       if (opts.validateSchema !== false) self.validateSchema(validate, true); | ||
|  |     } else if (inline) { | ||
|  |       validate = inline.call(self, it, rule.keyword, schema, parentSchema); | ||
|  |     } else { | ||
|  |       validate = rule.definition.validate; | ||
|  |       if (!validate) return; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (validate === undefined) | ||
|  |       throw new Error('custom keyword "' + rule.keyword + '"failed to compile'); | ||
|  | 
 | ||
|  |     var index = customRules.length; | ||
|  |     customRules[index] = validate; | ||
|  | 
 | ||
|  |     return { | ||
|  |       code: 'customRule' + index, | ||
|  |       validate: validate | ||
|  |     }; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * Checks if the schema is currently compiled | ||
|  |  * @this   Ajv | ||
|  |  * @param  {Object} schema schema to compile | ||
|  |  * @param  {Object} root root object | ||
|  |  * @param  {String} baseId base schema ID | ||
|  |  * @return {Object} object with properties "index" (compilation index) and "compiling" (boolean) | ||
|  |  */ | ||
|  | function checkCompiling(schema, root, baseId) { | ||
|  |   /* jshint validthis: true */ | ||
|  |   var index = compIndex.call(this, schema, root, baseId); | ||
|  |   if (index >= 0) return { index: index, compiling: true }; | ||
|  |   index = this._compilations.length; | ||
|  |   this._compilations[index] = { | ||
|  |     schema: schema, | ||
|  |     root: root, | ||
|  |     baseId: baseId | ||
|  |   }; | ||
|  |   return { index: index, compiling: false }; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * Removes the schema from the currently compiled list | ||
|  |  * @this   Ajv | ||
|  |  * @param  {Object} schema schema to compile | ||
|  |  * @param  {Object} root root object | ||
|  |  * @param  {String} baseId base schema ID | ||
|  |  */ | ||
|  | function endCompiling(schema, root, baseId) { | ||
|  |   /* jshint validthis: true */ | ||
|  |   var i = compIndex.call(this, schema, root, baseId); | ||
|  |   if (i >= 0) this._compilations.splice(i, 1); | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * Index of schema compilation in the currently compiled list | ||
|  |  * @this   Ajv | ||
|  |  * @param  {Object} schema schema to compile | ||
|  |  * @param  {Object} root root object | ||
|  |  * @param  {String} baseId base schema ID | ||
|  |  * @return {Integer} compilation index | ||
|  |  */ | ||
|  | function compIndex(schema, root, baseId) { | ||
|  |   /* jshint validthis: true */ | ||
|  |   for (var i=0; i<this._compilations.length; i++) { | ||
|  |     var c = this._compilations[i]; | ||
|  |     if (c.schema == schema && c.root == root && c.baseId == baseId) return i; | ||
|  |   } | ||
|  |   return -1; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | function patternCode(i, patterns) { | ||
|  |   return 'var pattern' + i + ' = new RegExp(' + util.toQuotedString(patterns[i]) + ');'; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | function defaultCode(i) { | ||
|  |   return 'var default' + i + ' = defaults[' + i + '];'; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | function refValCode(i, refVal) { | ||
|  |   return refVal[i] === undefined ? '' : 'var refVal' + i + ' = refVal[' + i + '];'; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | function customRuleCode(i) { | ||
|  |   return 'var customRule' + i + ' = customRules[' + i + '];'; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | function vars(arr, statement) { | ||
|  |   if (!arr.length) return ''; | ||
|  |   var code = ''; | ||
|  |   for (var i=0; i<arr.length; i++) | ||
|  |     code += statement(i, arr); | ||
|  |   return code; | ||
|  | } |