2017-05-03 15:35:00 +02:00
'use strict' ;
var compileSchema = require ( './compile' )
, resolve = require ( './compile/resolve' )
, Cache = require ( './cache' )
, SchemaObject = require ( './compile/schema_obj' )
2017-12-10 21:51:33 +01:00
, stableStringify = require ( 'fast-json-stable-stringify' )
2017-05-03 15:35:00 +02:00
, formats = require ( './compile/formats' )
, rules = require ( './compile/rules' )
2017-08-14 05:01:11 +02:00
, $dataMetaSchema = require ( './$data' )
, patternGroups = require ( './patternGroups' )
2017-05-03 15:35:00 +02:00
, util = require ( './compile/util' )
, co = require ( 'co' ) ;
module . exports = Ajv ;
2017-08-14 05:01:11 +02:00
Ajv . prototype . validate = validate ;
Ajv . prototype . compile = compile ;
Ajv . prototype . addSchema = addSchema ;
Ajv . prototype . addMetaSchema = addMetaSchema ;
Ajv . prototype . validateSchema = validateSchema ;
Ajv . prototype . getSchema = getSchema ;
Ajv . prototype . removeSchema = removeSchema ;
Ajv . prototype . addFormat = addFormat ;
Ajv . prototype . errorsText = errorsText ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
Ajv . prototype . _addSchema = _addSchema ;
Ajv . prototype . _compile = _compile ;
Ajv . prototype . compileAsync = require ( './compile/async' ) ;
2017-05-03 15:35:00 +02:00
var customKeyword = require ( './keyword' ) ;
Ajv . prototype . addKeyword = customKeyword . add ;
Ajv . prototype . getKeyword = customKeyword . get ;
Ajv . prototype . removeKeyword = customKeyword . remove ;
2017-08-14 05:01:11 +02:00
var errorClasses = require ( './compile/error_classes' ) ;
Ajv . ValidationError = errorClasses . Validation ;
Ajv . MissingRefError = errorClasses . MissingRef ;
Ajv . $dataMetaSchema = $dataMetaSchema ;
var META _SCHEMA _ID = 'http://json-schema.org/draft-06/schema' ;
2017-05-03 15:35:00 +02:00
var META _IGNORE _OPTIONS = [ 'removeAdditional' , 'useDefaults' , 'coerceTypes' ] ;
2017-08-14 05:01:11 +02:00
var META _SUPPORT _DATA = [ '/properties' ] ;
2017-05-03 15:35:00 +02:00
/ * *
* Creates validator instance .
* Usage : ` Ajv(opts) `
* @ param { Object } opts optional options
* @ return { Object } ajv instance
* /
function Ajv ( opts ) {
if ( ! ( this instanceof Ajv ) ) return new Ajv ( opts ) ;
opts = this . _opts = util . copy ( opts ) || { } ;
2017-12-10 21:51:33 +01:00
setLogger ( this ) ;
2017-05-03 15:35:00 +02:00
this . _schemas = { } ;
this . _refs = { } ;
this . _fragments = { } ;
this . _formats = formats ( opts . format ) ;
2017-08-14 05:01:11 +02:00
var schemaUriFormat = this . _schemaUriFormat = this . _formats [ 'uri-reference' ] ;
this . _schemaUriFormatFunc = function ( str ) { return schemaUriFormat . test ( str ) ; } ;
2017-05-03 15:35:00 +02:00
this . _cache = opts . cache || new Cache ;
this . _loadingSchemas = { } ;
this . _compilations = [ ] ;
this . RULES = rules ( ) ;
2017-08-14 05:01:11 +02:00
this . _getId = chooseGetId ( opts ) ;
2017-05-03 15:35:00 +02:00
opts . loopRequired = opts . loopRequired || Infinity ;
if ( opts . errorDataPath == 'property' ) opts . _errorDataPathProperty = true ;
2017-08-14 05:01:11 +02:00
if ( opts . serialize === undefined ) opts . serialize = stableStringify ;
this . _metaOpts = getMetaSchemaOptions ( this ) ;
if ( opts . formats ) addInitialFormats ( this ) ;
addDraft6MetaSchema ( this ) ;
if ( typeof opts . meta == 'object' ) this . addMetaSchema ( opts . meta ) ;
addInitialSchemas ( this ) ;
if ( opts . patternGroups ) patternGroups ( this ) ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/ * *
* Validate data using schema
2017-12-10 21:51:33 +01:00
* Schema will be compiled and cached ( using serialized JSON as key . [ fast - json - stable - stringify ] ( https : //github.com/epoberezkin/fast-json-stable-stringify) is used to serialize.
2017-08-14 05:01:11 +02:00
* @ this Ajv
* @ param { String | Object } schemaKeyRef key , ref or schema object
* @ param { Any } data to be validated
* @ return { Boolean } validation result . Errors from the last validation will be available in ` ajv.errors ` ( and also in compiled schema : ` schema.errors ` ) .
* /
function validate ( schemaKeyRef , data ) {
var v ;
if ( typeof schemaKeyRef == 'string' ) {
v = this . getSchema ( schemaKeyRef ) ;
if ( ! v ) throw new Error ( 'no schema with key or ref "' + schemaKeyRef + '"' ) ;
} else {
var schemaObj = this . _addSchema ( schemaKeyRef ) ;
v = schemaObj . validate || this . _compile ( schemaObj ) ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
var valid = v ( data ) ;
if ( v . $async === true )
return this . _opts . async == '*' ? co ( valid ) : valid ;
this . errors = v . errors ;
return valid ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/ * *
* Create validating function for passed schema .
* @ this Ajv
* @ param { Object } schema schema object
* @ param { Boolean } _meta true if schema is a meta - schema . Used internally to compile meta schemas of custom keywords .
* @ return { Function } validating function
* /
function compile ( schema , _meta ) {
var schemaObj = this . _addSchema ( schema , undefined , _meta ) ;
return schemaObj . validate || this . _compile ( schemaObj ) ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/ * *
* Adds schema to the instance .
* @ this Ajv
* @ param { Object | Array } schema schema or array of schemas . If array is passed , ` key ` and other parameters will be ignored .
* @ param { String } key Optional schema key . Can be passed to ` validate ` method instead of schema object or id / ref . One schema per instance can have empty ` id ` and ` key ` .
* @ param { Boolean } _skipValidation true to skip schema validation . Used internally , option validateSchema should be used instead .
* @ param { Boolean } _meta true if schema is a meta - schema . Used internally , addMetaSchema should be used instead .
2017-12-10 21:51:33 +01:00
* @ return { Ajv } this for method chaining
2017-08-14 05:01:11 +02:00
* /
function addSchema ( schema , key , _skipValidation , _meta ) {
if ( Array . isArray ( schema ) ) {
for ( var i = 0 ; i < schema . length ; i ++ ) this . addSchema ( schema [ i ] , undefined , _skipValidation , _meta ) ;
2017-12-10 21:51:33 +01:00
return this ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
var id = this . _getId ( schema ) ;
if ( id !== undefined && typeof id != 'string' )
throw new Error ( 'schema id must be string' ) ;
key = resolve . normalizeId ( key || id ) ;
checkUnique ( this , key ) ;
this . _schemas [ key ] = this . _addSchema ( schema , _skipValidation , _meta , true ) ;
2017-12-10 21:51:33 +01:00
return this ;
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/ * *
* Add schema that will be used to validate other schemas
* options in META _IGNORE _OPTIONS are alway set to false
* @ this Ajv
* @ param { Object } schema schema object
* @ param { String } key optional schema key
* @ param { Boolean } skipValidation true to skip schema validation , can be used to override validateSchema option for meta - schema
2017-12-10 21:51:33 +01:00
* @ return { Ajv } this for method chaining
2017-08-14 05:01:11 +02:00
* /
function addMetaSchema ( schema , key , skipValidation ) {
this . addSchema ( schema , key , skipValidation , true ) ;
2017-12-10 21:51:33 +01:00
return this ;
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/ * *
* Validate schema
* @ this Ajv
* @ param { Object } schema schema to validate
* @ param { Boolean } throwOrLogError pass true to throw ( or log ) an error if invalid
* @ return { Boolean } true if schema is valid
* /
function validateSchema ( schema , throwOrLogError ) {
var $schema = schema . $schema ;
if ( $schema !== undefined && typeof $schema != 'string' )
throw new Error ( '$schema must be a string' ) ;
$schema = $schema || this . _opts . defaultMeta || defaultMeta ( this ) ;
if ( ! $schema ) {
2017-12-10 21:51:33 +01:00
this . logger . warn ( 'meta-schema not available' ) ;
2017-08-14 05:01:11 +02:00
this . errors = null ;
return true ;
}
var currentUriFormat = this . _formats . uri ;
this . _formats . uri = typeof currentUriFormat == 'function'
? this . _schemaUriFormatFunc
: this . _schemaUriFormat ;
var valid ;
try { valid = this . validate ( $schema , schema ) ; }
finally { this . _formats . uri = currentUriFormat ; }
if ( ! valid && throwOrLogError ) {
var message = 'schema is invalid: ' + this . errorsText ( ) ;
2017-12-10 21:51:33 +01:00
if ( this . _opts . validateSchema == 'log' ) this . logger . error ( message ) ;
2017-08-14 05:01:11 +02:00
else throw new Error ( message ) ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
return valid ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function defaultMeta ( self ) {
var meta = self . _opts . meta ;
self . _opts . defaultMeta = typeof meta == 'object'
? self . _getId ( meta ) || meta
: self . getSchema ( META _SCHEMA _ID )
? META _SCHEMA _ID
: undefined ;
return self . _opts . defaultMeta ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/ * *
* Get compiled schema from the instance by ` key ` or ` ref ` .
* @ this Ajv
* @ param { String } keyRef ` key ` that was passed to ` addSchema ` or full schema reference ( ` schema.id ` or resolved id ) .
* @ return { Function } schema validating function ( with property ` schema ` ) .
* /
function getSchema ( keyRef ) {
var schemaObj = _getSchemaObj ( this , keyRef ) ;
switch ( typeof schemaObj ) {
case 'object' : return schemaObj . validate || this . _compile ( schemaObj ) ;
case 'string' : return this . getSchema ( schemaObj ) ;
case 'undefined' : return _getSchemaFragment ( this , keyRef ) ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function _getSchemaFragment ( self , ref ) {
var res = resolve . schema . call ( self , { schema : { } } , ref ) ;
if ( res ) {
var schema = res . schema
, root = res . root
, baseId = res . baseId ;
var v = compileSchema . call ( self , schema , root , undefined , baseId ) ;
self . _fragments [ ref ] = new SchemaObject ( {
ref : ref ,
fragment : true ,
schema : schema ,
root : root ,
baseId : baseId ,
validate : v
} ) ;
return v ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function _getSchemaObj ( self , keyRef ) {
keyRef = resolve . normalizeId ( keyRef ) ;
return self . _schemas [ keyRef ] || self . _refs [ keyRef ] || self . _fragments [ keyRef ] ;
}
/ * *
* Remove cached schema ( s ) .
* If no parameter is passed all schemas but meta - schemas are removed .
* If RegExp is passed all schemas with key / id matching pattern but meta - schemas are removed .
* Even if schema is referenced by other schemas it still can be removed as other schemas have local references .
* @ this Ajv
* @ param { String | Object | RegExp } schemaKeyRef key , ref , pattern to match key / ref or schema object
2017-12-10 21:51:33 +01:00
* @ return { Ajv } this for method chaining
2017-08-14 05:01:11 +02:00
* /
function removeSchema ( schemaKeyRef ) {
if ( schemaKeyRef instanceof RegExp ) {
_removeAllSchemas ( this , this . _schemas , schemaKeyRef ) ;
_removeAllSchemas ( this , this . _refs , schemaKeyRef ) ;
2017-12-10 21:51:33 +01:00
return this ;
2017-08-14 05:01:11 +02:00
}
switch ( typeof schemaKeyRef ) {
case 'undefined' :
_removeAllSchemas ( this , this . _schemas ) ;
_removeAllSchemas ( this , this . _refs ) ;
this . _cache . clear ( ) ;
2017-12-10 21:51:33 +01:00
return this ;
2017-08-14 05:01:11 +02:00
case 'string' :
var schemaObj = _getSchemaObj ( this , schemaKeyRef ) ;
if ( schemaObj ) this . _cache . del ( schemaObj . cacheKey ) ;
delete this . _schemas [ schemaKeyRef ] ;
delete this . _refs [ schemaKeyRef ] ;
2017-12-10 21:51:33 +01:00
return this ;
2017-08-14 05:01:11 +02:00
case 'object' :
var serialize = this . _opts . serialize ;
var cacheKey = serialize ? serialize ( schemaKeyRef ) : schemaKeyRef ;
this . _cache . del ( cacheKey ) ;
var id = this . _getId ( schemaKeyRef ) ;
if ( id ) {
id = resolve . normalizeId ( id ) ;
delete this . _schemas [ id ] ;
delete this . _refs [ id ] ;
}
2017-05-03 15:35:00 +02:00
}
2017-12-10 21:51:33 +01:00
return this ;
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function _removeAllSchemas ( self , schemas , regex ) {
for ( var keyRef in schemas ) {
var schemaObj = schemas [ keyRef ] ;
if ( ! schemaObj . meta && ( ! regex || regex . test ( keyRef ) ) ) {
self . _cache . del ( schemaObj . cacheKey ) ;
delete schemas [ keyRef ] ;
2017-05-03 15:35:00 +02:00
}
}
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/* @this Ajv */
function _addSchema ( schema , skipValidation , meta , shouldAddSchema ) {
if ( typeof schema != 'object' && typeof schema != 'boolean' )
throw new Error ( 'schema should be object or boolean' ) ;
var serialize = this . _opts . serialize ;
var cacheKey = serialize ? serialize ( schema ) : schema ;
var cached = this . _cache . get ( cacheKey ) ;
if ( cached ) return cached ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
shouldAddSchema = shouldAddSchema || this . _opts . addUsedSchema !== false ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
var id = resolve . normalizeId ( this . _getId ( schema ) ) ;
if ( id && shouldAddSchema ) checkUnique ( this , id ) ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
var willValidate = this . _opts . validateSchema !== false && ! skipValidation ;
var recursiveMeta ;
if ( willValidate && ! ( recursiveMeta = id && id == resolve . normalizeId ( schema . $schema ) ) )
this . validateSchema ( schema , true ) ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
var localRefs = resolve . ids . call ( this , schema ) ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
var schemaObj = new SchemaObject ( {
id : id ,
schema : schema ,
localRefs : localRefs ,
cacheKey : cacheKey ,
meta : meta
} ) ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
if ( id [ 0 ] != '#' && shouldAddSchema ) this . _refs [ id ] = schemaObj ;
this . _cache . put ( cacheKey , schemaObj ) ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
if ( willValidate && recursiveMeta ) this . validateSchema ( schema , true ) ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
return schemaObj ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/* @this Ajv */
function _compile ( schemaObj , root ) {
if ( schemaObj . compiling ) {
schemaObj . validate = callValidate ;
callValidate . schema = schemaObj . schema ;
callValidate . errors = null ;
callValidate . root = root ? root : callValidate ;
if ( schemaObj . schema . $async === true )
callValidate . $async = true ;
return callValidate ;
}
schemaObj . compiling = true ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
var currentOpts ;
if ( schemaObj . meta ) {
currentOpts = this . _opts ;
this . _opts = this . _metaOpts ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
var v ;
try { v = compileSchema . call ( this , schemaObj . schema , root , schemaObj . localRefs ) ; }
finally {
schemaObj . compiling = false ;
if ( schemaObj . meta ) this . _opts = currentOpts ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
schemaObj . validate = v ;
schemaObj . refs = v . refs ;
schemaObj . refVal = v . refVal ;
schemaObj . root = v . root ;
return v ;
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function callValidate ( ) {
var _validate = schemaObj . validate ;
var result = _validate . apply ( null , arguments ) ;
callValidate . errors = _validate . errors ;
return result ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function chooseGetId ( opts ) {
switch ( opts . schemaId ) {
case '$id' : return _get$Id ;
case 'id' : return _getId ;
default : return _get$IdOrId ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-12-10 21:51:33 +01:00
/* @this Ajv */
2017-08-14 05:01:11 +02:00
function _getId ( schema ) {
2017-12-10 21:51:33 +01:00
if ( schema . $id ) this . logger . warn ( 'schema $id ignored' , schema . $id ) ;
2017-08-14 05:01:11 +02:00
return schema . id ;
}
2017-05-03 15:35:00 +02:00
2017-12-10 21:51:33 +01:00
/* @this Ajv */
2017-08-14 05:01:11 +02:00
function _get$Id ( schema ) {
2017-12-10 21:51:33 +01:00
if ( schema . id ) this . logger . warn ( 'schema id ignored' , schema . id ) ;
2017-08-14 05:01:11 +02:00
return schema . $id ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function _get$IdOrId ( schema ) {
if ( schema . $id && schema . id && schema . $id != schema . id )
throw new Error ( 'schema $id is different from id' ) ;
return schema . $id || schema . id ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
/ * *
* Convert array of error message objects to string
* @ this Ajv
* @ param { Array < Object > } errors optional array of validation errors , if not passed errors from the instance are used .
* @ param { Object } options optional options with properties ` separator ` and ` dataVar ` .
* @ return { String } human readable string with all errors descriptions
* /
function errorsText ( errors , options ) {
errors = errors || this . errors ;
if ( ! errors ) return 'No errors' ;
options = options || { } ;
var separator = options . separator === undefined ? ', ' : options . separator ;
var dataVar = options . dataVar === undefined ? 'data' : options . dataVar ;
var text = '' ;
for ( var i = 0 ; i < errors . length ; i ++ ) {
var e = errors [ i ] ;
if ( e ) text += dataVar + e . dataPath + ' ' + e . message + separator ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
return text . slice ( 0 , - separator . length ) ;
}
/ * *
* Add custom format
* @ this Ajv
* @ param { String } name format name
* @ param { String | RegExp | Function } format string is converted to RegExp ; function should return boolean ( true when valid )
2017-12-10 21:51:33 +01:00
* @ return { Ajv } this for method chaining
2017-08-14 05:01:11 +02:00
* /
function addFormat ( name , format ) {
if ( typeof format == 'string' ) format = new RegExp ( format ) ;
this . _formats [ name ] = format ;
2017-12-10 21:51:33 +01:00
return this ;
2017-08-14 05:01:11 +02:00
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function addDraft6MetaSchema ( self ) {
var $dataSchema ;
if ( self . _opts . $data ) {
$dataSchema = require ( './refs/$data.json' ) ;
self . addMetaSchema ( $dataSchema , $dataSchema . $id , true ) ;
2017-05-03 15:35:00 +02:00
}
2017-08-14 05:01:11 +02:00
if ( self . _opts . meta === false ) return ;
var metaSchema = require ( './refs/json-schema-draft-06.json' ) ;
if ( self . _opts . $data ) metaSchema = $dataMetaSchema ( metaSchema , META _SUPPORT _DATA ) ;
self . addMetaSchema ( metaSchema , META _SCHEMA _ID , true ) ;
self . _refs [ 'http://json-schema.org/schema' ] = META _SCHEMA _ID ;
}
function addInitialSchemas ( self ) {
var optsSchemas = self . _opts . schemas ;
if ( ! optsSchemas ) return ;
if ( Array . isArray ( optsSchemas ) ) self . addSchema ( optsSchemas ) ;
else for ( var key in optsSchemas ) self . addSchema ( optsSchemas [ key ] , key ) ;
}
2017-05-03 15:35:00 +02:00
2017-08-14 05:01:11 +02:00
function addInitialFormats ( self ) {
for ( var name in self . _opts . formats ) {
var format = self . _opts . formats [ name ] ;
self . addFormat ( name , format ) ;
2017-05-03 15:35:00 +02:00
}
}
2017-08-14 05:01:11 +02:00
function checkUnique ( self , id ) {
if ( self . _schemas [ id ] || self . _refs [ id ] )
throw new Error ( 'schema with key or id "' + id + '" already exists' ) ;
}
function getMetaSchemaOptions ( self ) {
var metaOpts = util . copy ( self . _opts ) ;
for ( var i = 0 ; i < META _IGNORE _OPTIONS . length ; i ++ )
delete metaOpts [ META _IGNORE _OPTIONS [ i ] ] ;
return metaOpts ;
}
2017-12-10 21:51:33 +01:00
function setLogger ( self ) {
var logger = self . _opts . logger ;
if ( logger === false ) {
self . logger = { log : noop , warn : noop , error : noop } ;
} else {
if ( logger === undefined ) logger = console ;
if ( ! ( typeof logger == 'object' && logger . log && logger . warn && logger . error ) )
throw new Error ( 'logger must implement log, warn and error methods' ) ;
self . logger = logger ;
}
}
function noop ( ) { }