2023-10-03 11:14:36 +08:00
export {
Format ,
FormatDefinition ,
AsyncFormatDefinition ,
KeywordDefinition ,
KeywordErrorDefinition ,
CodeKeywordDefinition ,
MacroKeywordDefinition ,
FuncKeywordDefinition ,
Vocabulary ,
Schema ,
SchemaObject ,
AnySchemaObject ,
AsyncSchema ,
AnySchema ,
ValidateFunction ,
AsyncValidateFunction ,
AnyValidateFunction ,
ErrorObject ,
ErrorNoParams ,
} from "./types"
export { SchemaCxt , SchemaObjCxt } from "./compile"
export interface Plugin < Opts > {
( ajv : Ajv , options? : Opts ) : Ajv
[ prop : string ] : any
}
import KeywordCxt from "./compile/context"
export { KeywordCxt }
export { DefinedError } from "./vocabularies/errors"
export { JSONType } from "./compile/rules"
export { JSONSchemaType } from "./types/json-schema"
export { JTDSchemaType } from "./types/jtd-schema"
export { _ , str , stringify , nil , Name , Code , CodeGen , CodeGenOptions } from "./compile/codegen"
import type {
Schema ,
AnySchema ,
AnySchemaObject ,
SchemaObject ,
AsyncSchema ,
Vocabulary ,
KeywordDefinition ,
AddedKeywordDefinition ,
AnyValidateFunction ,
ValidateFunction ,
AsyncValidateFunction ,
ErrorObject ,
Format ,
AddedFormat ,
} from "./types"
import type { JSONSchemaType } from "./types/json-schema"
import type { JTDSchemaType } from "./types/jtd-schema"
import { ValidationError , MissingRefError } from "./compile/error_classes"
import { getRules , ValidationRules , Rule , RuleGroup , JSONType } from "./compile/rules"
import { SchemaEnv , compileSchema , resolveSchema } from "./compile"
import { Code , ValueScope } from "./compile/codegen"
import { normalizeId , getSchemaRefs } from "./compile/resolve"
import { getJSONTypes } from "./compile/validate/dataType"
import { eachItem } from "./compile/util"
import * as $dataRefSchema from "./refs/data.json"
const META_IGNORE_OPTIONS : ( keyof Options ) [ ] = [ "removeAdditional" , "useDefaults" , "coerceTypes" ]
const EXT_SCOPE_NAMES = new Set ( [
"validate" ,
"serialize" ,
"parse" ,
"wrapper" ,
"root" ,
"schema" ,
"keyword" ,
"pattern" ,
"formats" ,
"validate$data" ,
"func" ,
"obj" ,
"Error" ,
] )
export type Options = CurrentOptions & DeprecatedOptions
export interface CurrentOptions {
// strict mode options (NEW)
strict? : boolean | "log"
strictTypes? : boolean | "log"
strictTuples? : boolean | "log"
strictRequired? : boolean | "log"
allowMatchingProperties? : boolean // disables a strict mode restriction
allowUnionTypes? : boolean
validateFormats? : boolean
// validation and reporting options:
$data? : boolean
allErrors? : boolean
verbose? : boolean
$comment ? :
| true
| ( ( comment : string , schemaPath? : string , rootSchema? : AnySchemaObject ) = > unknown )
formats ? : { [ Name in string ] ? : Format }
keywords? : Vocabulary
schemas? : AnySchema [ ] | { [ Key in string ] ? : AnySchema }
logger? : Logger | false
loadSchema ? : ( uri : string ) = > Promise < AnySchemaObject >
// options to modify validated data:
removeAdditional? : boolean | "all" | "failing"
useDefaults? : boolean | "empty"
coerceTypes? : boolean | "array"
// advanced options:
next? : boolean // NEW
unevaluated? : boolean // NEW
dynamicRef? : boolean // NEW
jtd? : boolean // NEW
meta? : SchemaObject | boolean
defaultMeta? : string | AnySchemaObject
validateSchema? : boolean | "log"
addUsedSchema? : boolean
inlineRefs? : boolean | number
passContext? : boolean
loopRequired? : number
loopEnum? : number // NEW
ownProperties? : boolean
multipleOfPrecision? : number
messages? : boolean
code? : CodeOptions // NEW
ajvErrors? : boolean
}
export interface CodeOptions {
es5? : boolean
lines? : boolean
optimize? : boolean | number
formats? : Code // code to require (or construct) map of available formats - for standalone code
source? : boolean
process ? : ( code : string , schema? : SchemaEnv ) = > string
}
interface InstanceCodeOptions extends CodeOptions {
optimize : number
}
interface DeprecatedOptions {
/** @deprecated */
ignoreKeywordsWithRef? : boolean
/** @deprecated */
jsPropertySyntax? : boolean // added instead of jsonPointers
/** @deprecated */
unicode? : boolean
}
interface RemovedOptions {
format? : boolean
errorDataPath ? : "object" | "property"
nullable? : boolean // "nullable" keyword is supported by default
jsonPointers? : boolean
extendRefs? : true | "ignore" | "fail"
missingRefs? : true | "ignore" | "fail"
processCode ? : ( code : string , schema? : SchemaEnv ) = > string
sourceCode? : boolean
schemaId? : string
strictDefaults? : boolean
strictKeywords? : boolean
strictNumbers? : boolean
uniqueItems? : boolean
unknownFormats? : true | string [ ] | "ignore"
cache? : any
serialize ? : ( schema : AnySchema ) = > unknown
}
type OptionsInfo < T extends RemovedOptions | DeprecatedOptions > = {
[ K in keyof T ] - ? : string | undefined
}
const removedOptions : OptionsInfo < RemovedOptions > = {
errorDataPath : "" ,
format : "`validateFormats: false` can be used instead." ,
nullable : '"nullable" keyword is supported by default.' ,
jsonPointers : "Deprecated jsPropertySyntax can be used instead." ,
extendRefs : "Deprecated ignoreKeywordsWithRef can be used instead." ,
missingRefs : "Pass empty schema with $id that should be ignored to ajv.addSchema." ,
processCode : "Use option `code: {process: (code, schemaEnv: object) => string}`" ,
sourceCode : "Use option `code: {source: true}`" ,
schemaId : "JSON Schema draft-04 is not supported in Ajv v7." ,
strictDefaults : "It is default now, see option `strict`." ,
strictKeywords : "It is default now, see option `strict`." ,
strictNumbers : "It is default now, see option `strict`." ,
uniqueItems : '"uniqueItems" keyword is always validated.' ,
unknownFormats : "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option)." ,
cache : "Map is used as cache, schema object as key." ,
serialize : "Map is used as cache, schema object as key." ,
}
const deprecatedOptions : OptionsInfo < DeprecatedOptions > = {
ignoreKeywordsWithRef : "" ,
jsPropertySyntax : "" ,
unicode : '"minLength"/"maxLength" account for unicode characters by default.' ,
}
type RequiredInstanceOptions = {
[ K in
| "strict"
| "strictTypes"
| "strictTuples"
| "inlineRefs"
| "loopRequired"
| "loopEnum"
| "meta"
| "messages"
| "addUsedSchema"
| "validateSchema"
| "validateFormats" ] : NonNullable < Options [ K ] >
} & { code : InstanceCodeOptions }
export type InstanceOptions = Options & RequiredInstanceOptions
function requiredOptions ( o : Options ) : RequiredInstanceOptions {
const strict = o . strict ? ? true
const strictLog = strict ? "log" : false
const _optz = o . code ? . optimize
const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0
return {
strict ,
strictTypes : o.strictTypes ? ? strictLog ,
strictTuples : o.strictTuples ? ? strictLog ,
code : o.code ? { . . . o . code , optimize } : { optimize } ,
loopRequired : o.loopRequired ? ? Infinity ,
loopEnum : o.loopEnum ? ? Infinity ,
meta : o.meta ? ? true ,
messages : o.messages ? ? true ,
inlineRefs : o.inlineRefs ? ? true ,
addUsedSchema : o.addUsedSchema ? ? true ,
validateSchema : o.validateSchema ? ? true ,
validateFormats : o.validateFormats ? ? true ,
}
}
export interface Logger {
log ( . . . args : unknown [ ] ) : unknown
warn ( . . . args : unknown [ ] ) : unknown
error ( . . . args : unknown [ ] ) : unknown
}
export default class Ajv {
opts : InstanceOptions
errors? : ErrorObject [ ] | null // errors from the last validation
logger : Logger
// shared external scope values for compiled functions
readonly scope : ValueScope
readonly schemas : { [ Key in string ] ? : SchemaEnv } = { }
readonly refs : { [ Ref in string ] ? : SchemaEnv | string } = { }
readonly formats : { [ Name in string ] ? : AddedFormat } = { }
readonly RULES : ValidationRules
readonly _compilations : Set < SchemaEnv > = new Set ( )
private readonly _loading : { [ Ref in string ] ? : Promise < AnySchemaObject > } = { }
private readonly _cache : Map < AnySchema , SchemaEnv > = new Map ( )
private readonly _metaOpts : InstanceOptions
static ValidationError = ValidationError
static MissingRefError = MissingRefError
constructor ( opts : Options = { } ) {
opts = this . opts = { . . . opts , . . . requiredOptions ( opts ) }
const { es5 , lines } = this . opts . code
this . scope = new ValueScope ( { scope : { } , prefixes : EXT_SCOPE_NAMES , es5 , lines } )
this . logger = getLogger ( opts . logger )
const formatOpt = opts . validateFormats
opts . validateFormats = false
this . RULES = getRules ( )
checkOptions . call ( this , removedOptions , opts , "NOT SUPPORTED" )
checkOptions . call ( this , deprecatedOptions , opts , "DEPRECATED" , "warn" )
this . _metaOpts = getMetaSchemaOptions . call ( this )
if ( opts . formats ) addInitialFormats . call ( this )
this . _addVocabularies ( )
this . _addDefaultMetaSchema ( )
if ( opts . keywords ) addInitialKeywords . call ( this , opts . keywords )
if ( typeof opts . meta == "object" ) this . addMetaSchema ( opts . meta )
addInitialSchemas . call ( this )
opts . validateFormats = formatOpt
}
_addVocabularies ( ) : void {
this . addKeyword ( "$async" )
}
_addDefaultMetaSchema ( ) : void {
const { $data , meta } = this . opts
if ( meta && $data ) this . addMetaSchema ( $dataRefSchema , $dataRefSchema . $id , false )
}
defaultMeta ( ) : string | AnySchemaObject | undefined {
const { meta } = this . opts
return ( this . opts . defaultMeta = typeof meta == "object" ? meta . $id || meta : undefined )
}
// Validate data using schema
// AnySchema will be compiled and cached using schema itself as a key for Map
validate ( schema : Schema | string , data : unknown ) : boolean
validate ( schemaKeyRef : AnySchema | string , data : unknown ) : boolean | Promise < unknown >
validate < T > ( schema : Schema | JSONSchemaType < T > | string , data : unknown ) : data is T
// Separated for type inference to work
// eslint-disable-next-line @typescript-eslint/unified-signatures
validate < T > ( schema : JTDSchemaType < T > , data : unknown ) : data is T
validate < T > ( schema : AsyncSchema , data : unknown | T ) : Promise < T >
validate < T > ( schemaKeyRef : AnySchema | string , data : unknown ) : data is T | Promise < T >
validate < T > (
schemaKeyRef : AnySchema | string , // key, ref or schema object
data : unknown | T // to be validated
) : boolean | Promise < T > {
let v : AnyValidateFunction | undefined
if ( typeof schemaKeyRef == "string" ) {
v = this . getSchema < T > ( schemaKeyRef )
if ( ! v ) throw new Error ( ` no schema with key or ref " ${ schemaKeyRef } " ` )
} else {
v = this . compile < T > ( schemaKeyRef )
}
const valid = v ( data )
if ( ! ( "$async" in v ) ) this . errors = v . errors
return valid
}
// Create validation function for passed schema
// _meta: true if schema is a meta-schema. Used internally to compile meta schemas of user-defined keywords.
compile < T = unknown > ( schema : Schema | JSONSchemaType < T > , _meta? : boolean ) : ValidateFunction < T >
// Separated for type inference to work
// eslint-disable-next-line @typescript-eslint/unified-signatures
compile < T = unknown > ( schema : JTDSchemaType < T > , _meta? : boolean ) : ValidateFunction < T >
compile < T = unknown > ( schema : AsyncSchema , _meta? : boolean ) : AsyncValidateFunction < T >
compile < T = unknown > ( schema : AnySchema , _meta? : boolean ) : AnyValidateFunction < T >
compile < T = unknown > ( schema : AnySchema , _meta? : boolean ) : AnyValidateFunction < T > {
const sch = this . _addSchema ( schema , _meta )
return ( sch . validate || this . _compileSchemaEnv ( sch ) ) as AnyValidateFunction < T >
}
// Creates validating function for passed schema with asynchronous loading of missing schemas.
// `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema.
// TODO allow passing schema URI
// meta - optional true to compile meta-schema
compileAsync < T = unknown > (
schema : SchemaObject | JSONSchemaType < T > ,
_meta? : boolean
) : Promise < ValidateFunction < T > >
// Separated for type inference to work
// eslint-disable-next-line @typescript-eslint/unified-signatures
compileAsync < T = unknown > ( schema : JTDSchemaType < T > , _meta? : boolean ) : Promise < ValidateFunction < T > >
compileAsync < T = unknown > ( schema : AsyncSchema , meta? : boolean ) : Promise < AsyncValidateFunction < T > >
// eslint-disable-next-line @typescript-eslint/unified-signatures
compileAsync < T = unknown > (
schema : AnySchemaObject ,
meta? : boolean
) : Promise < AnyValidateFunction < T > >
compileAsync < T = unknown > (
schema : AnySchemaObject ,
meta? : boolean
) : Promise < AnyValidateFunction < T > > {
if ( typeof this . opts . loadSchema != "function" ) {
throw new Error ( "options.loadSchema should be a function" )
}
const { loadSchema } = this . opts
return runCompileAsync . call ( this , schema , meta )
async function runCompileAsync (
this : Ajv ,
_schema : AnySchemaObject ,
_meta? : boolean
) : Promise < AnyValidateFunction > {
await loadMetaSchema . call ( this , _schema . $schema )
const sch = this . _addSchema ( _schema , _meta )
return sch . validate || _compileAsync . call ( this , sch )
}
async function loadMetaSchema ( this : Ajv , $ref? : string ) : Promise < void > {
if ( $ref && ! this . getSchema ( $ref ) ) {
await runCompileAsync . call ( this , { $ref } , true )
}
}
async function _compileAsync ( this : Ajv , sch : SchemaEnv ) : Promise < AnyValidateFunction > {
try {
return this . _compileSchemaEnv ( sch )
} catch ( e ) {
if ( ! ( e instanceof MissingRefError ) ) throw e
checkLoaded . call ( this , e )
await loadMissingSchema . call ( this , e . missingSchema )
return _compileAsync . call ( this , sch )
}
}
function checkLoaded ( this : Ajv , { missingSchema : ref , missingRef } : MissingRefError ) : void {
if ( this . refs [ ref ] ) {
throw new Error ( ` AnySchema ${ ref } is loaded but ${ missingRef } cannot be resolved ` )
}
}
async function loadMissingSchema ( this : Ajv , ref : string ) : Promise < void > {
const _schema = await _loadSchema . call ( this , ref )
if ( ! this . refs [ ref ] ) await loadMetaSchema . call ( this , _schema . $schema )
if ( ! this . refs [ ref ] ) this . addSchema ( _schema , ref , meta )
}
async function _loadSchema ( this : Ajv , ref : string ) : Promise < AnySchemaObject > {
const p = this . _loading [ ref ]
if ( p ) return p
try {
return await ( this . _loading [ ref ] = loadSchema ( ref ) )
} finally {
delete this . _loading [ ref ]
}
}
}
// Adds schema to the instance
addSchema (
schema : AnySchema | AnySchema [ ] , // If array is passed, `key` will be ignored
key? : string , // 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`.
_meta? : boolean , // true if schema is a meta-schema. Used internally, addMetaSchema should be used instead.
_validateSchema = this . opts . validateSchema // false to skip schema validation. Used internally, option validateSchema should be used instead.
) : Ajv {
if ( Array . isArray ( schema ) ) {
for ( const sch of schema ) this . addSchema ( sch , undefined , _meta , _validateSchema )
return this
}
let id : string | undefined
if ( typeof schema === "object" ) {
id = schema . $id
if ( id !== undefined && typeof id != "string" ) throw new Error ( "schema id must be string" )
}
key = normalizeId ( key || id )
this . _checkUnique ( key )
this . schemas [ key ] = this . _addSchema ( schema , _meta , _validateSchema , true )
return this
}
// Add schema that will be used to validate other schemas
// options in META_IGNORE_OPTIONS are alway set to false
addMetaSchema (
schema : AnySchemaObject ,
key? : string , // schema key
_validateSchema = this . opts . validateSchema // false to skip schema validation, can be used to override validateSchema option for meta-schema
) : Ajv {
this . addSchema ( schema , key , true , _validateSchema )
return this
}
// Validate schema against its meta-schema
validateSchema ( schema : AnySchema , throwOrLogError? : boolean ) : boolean | Promise < unknown > {
if ( typeof schema == "boolean" ) return true
let $schema : string | AnySchemaObject | undefined
$schema = schema . $schema
if ( $schema !== undefined && typeof $schema != "string" ) {
throw new Error ( "$schema must be a string" )
}
$schema = $schema || this . opts . defaultMeta || this . defaultMeta ( )
if ( ! $schema ) {
this . logger . warn ( "meta-schema not available" )
this . errors = null
return true
}
const valid = this . validate ( $schema , schema )
if ( ! valid && throwOrLogError ) {
const message = "schema is invalid: " + this . errorsText ( )
if ( this . opts . validateSchema === "log" ) this . logger . error ( message )
else throw new Error ( message )
}
return valid
}
// Get compiled schema by `key` or `ref`.
// (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id)
getSchema < T = unknown > ( keyRef : string ) : AnyValidateFunction < T > | undefined {
let sch
while ( typeof ( sch = getSchEnv . call ( this , keyRef ) ) == "string" ) keyRef = sch
if ( sch === undefined ) {
const root = new SchemaEnv ( { schema : { } } )
sch = resolveSchema . call ( this , root , keyRef )
if ( ! sch ) return
this . refs [ keyRef ] = sch
}
return ( sch . validate || this . _compileSchemaEnv ( sch ) ) as AnyValidateFunction < T > | undefined
}
// 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.
removeSchema ( schemaKeyRef? : AnySchema | string | RegExp ) : Ajv {
if ( schemaKeyRef instanceof RegExp ) {
this . _removeAllSchemas ( this . schemas , schemaKeyRef )
this . _removeAllSchemas ( this . refs , schemaKeyRef )
return this
}
switch ( typeof schemaKeyRef ) {
case "undefined" :
this . _removeAllSchemas ( this . schemas )
this . _removeAllSchemas ( this . refs )
this . _cache . clear ( )
return this
case "string" : {
const sch = getSchEnv . call ( this , schemaKeyRef )
if ( typeof sch == "object" ) this . _cache . delete ( sch . schema )
delete this . schemas [ schemaKeyRef ]
delete this . refs [ schemaKeyRef ]
return this
}
case "object" : {
const cacheKey = schemaKeyRef
this . _cache . delete ( cacheKey )
let id = schemaKeyRef . $id
if ( id ) {
id = normalizeId ( id )
delete this . schemas [ id ]
delete this . refs [ id ]
}
return this
}
default :
throw new Error ( "ajv.removeSchema: invalid parameter" )
}
}
// add "vocabulary" - a collection of keywords
addVocabulary ( definitions : Vocabulary ) : Ajv {
for ( const def of definitions ) this . addKeyword ( def )
return this
}
addKeyword (
kwdOrDef : string | KeywordDefinition ,
def? : KeywordDefinition // deprecated
) : Ajv {
let keyword : string | string [ ]
if ( typeof kwdOrDef == "string" ) {
keyword = kwdOrDef
if ( typeof def == "object" ) {
this . logger . warn ( "these parameters are deprecated, see docs for addKeyword" )
def . keyword = keyword
}
} else if ( typeof kwdOrDef == "object" && def === undefined ) {
def = kwdOrDef
keyword = def . keyword
if ( Array . isArray ( keyword ) && ! keyword . length ) {
throw new Error ( "addKeywords: keyword must be string or non-empty array" )
}
} else {
throw new Error ( "invalid addKeywords parameters" )
}
checkKeyword . call ( this , keyword , def )
if ( ! def ) {
eachItem ( keyword , ( kwd ) = > addRule . call ( this , kwd ) )
return this
}
keywordMetaschema . call ( this , def )
const definition : AddedKeywordDefinition = {
. . . def ,
type : getJSONTypes ( def . type ) ,
schemaType : getJSONTypes ( def . schemaType ) ,
}
eachItem (
keyword ,
definition . type . length === 0
? ( k ) = > addRule . call ( this , k , definition )
: ( k ) = > definition . type . forEach ( ( t ) = > addRule . call ( this , k , definition , t ) )
)
return this
}
getKeyword ( keyword : string ) : AddedKeywordDefinition | boolean {
const rule = this . RULES . all [ keyword ]
return typeof rule == "object" ? rule . definition : ! ! rule
}
// Remove keyword
removeKeyword ( keyword : string ) : Ajv {
// TODO return type should be Ajv
const { RULES } = this
delete RULES . keywords [ keyword ]
delete RULES . all [ keyword ]
for ( const group of RULES . rules ) {
const i = group . rules . findIndex ( ( rule ) = > rule . keyword === keyword )
if ( i >= 0 ) group . rules . splice ( i , 1 )
}
return this
}
// Add format
addFormat ( name : string , format : Format ) : Ajv {
if ( typeof format == "string" ) format = new RegExp ( format )
this . formats [ name ] = format
return this
}
errorsText (
errors : ErrorObject [ ] | null | undefined = this . errors , // optional array of validation errors
{ separator = ", " , dataVar = "data" } : ErrorsTextOptions = { } // optional options with properties `separator` and `dataVar`
) : string {
if ( ! errors || errors . length === 0 ) return "No errors"
return errors
. map ( ( e ) = > ` ${ dataVar } ${ e . dataPath } ${ e . message } ` )
. reduce ( ( text , msg ) = > text + separator + msg )
}
$dataMetaSchema ( metaSchema : AnySchemaObject , keywordsJsonPointers : string [ ] ) : AnySchemaObject {
const rules = this . RULES . all
metaSchema = JSON . parse ( JSON . stringify ( metaSchema ) )
for ( const jsonPointer of keywordsJsonPointers ) {
const segments = jsonPointer . split ( "/" ) . slice ( 1 ) // first segment is an empty string
let keywords = metaSchema
for ( const seg of segments ) keywords = keywords [ seg ] as AnySchemaObject
for ( const key in rules ) {
const rule = rules [ key ]
if ( typeof rule != "object" ) continue
const { $data } = rule . definition
const schema = keywords [ key ] as AnySchemaObject | undefined
if ( $data && schema ) keywords [ key ] = schemaOrData ( schema )
}
}
return metaSchema
}
private _removeAllSchemas ( schemas : { [ Ref in string ] ? : SchemaEnv | string } , regex? : RegExp ) : void {
for ( const keyRef in schemas ) {
const sch = schemas [ keyRef ]
if ( ! regex || regex . test ( keyRef ) ) {
if ( typeof sch == "string" ) {
delete schemas [ keyRef ]
} else if ( sch && ! sch . meta ) {
this . _cache . delete ( sch . schema )
delete schemas [ keyRef ]
}
}
}
}
_addSchema (
schema : AnySchema ,
meta? : boolean ,
validateSchema = this . opts . validateSchema ,
addSchema = this . opts . addUsedSchema
) : SchemaEnv {
if ( typeof schema != "object" ) {
if ( this . opts . jtd ) throw new Error ( "schema must be object" )
else if ( typeof schema != "boolean" ) throw new Error ( "schema must be object or boolean" )
}
let sch = this . _cache . get ( schema )
if ( sch !== undefined ) return sch
const localRefs = getSchemaRefs . call ( this , schema )
sch = new SchemaEnv ( { schema , meta , localRefs } )
this . _cache . set ( sch . schema , sch )
const id = sch . baseId
if ( addSchema && ! id . startsWith ( "#" ) ) {
// TODO atm it is allowed to overwrite schemas without id (instead of not adding them)
if ( id ) this . _checkUnique ( id )
this . refs [ id ] = sch
}
if ( validateSchema ) this . validateSchema ( schema , true )
return sch
}
private _checkUnique ( id : string ) : void {
if ( this . schemas [ id ] || this . refs [ id ] ) {
throw new Error ( ` schema with key or id " ${ id } " already exists ` )
}
}
private _compileSchemaEnv ( sch : SchemaEnv ) : AnyValidateFunction {
if ( sch . meta ) this . _compileMetaSchema ( sch )
else compileSchema . call ( this , sch )
/* istanbul ignore if */
if ( ! sch . validate ) throw new Error ( "ajv implementation error" )
return sch . validate
}
private _compileMetaSchema ( sch : SchemaEnv ) : void {
const currentOpts = this . opts
this . opts = this . _metaOpts
try {
compileSchema . call ( this , sch )
} finally {
this . opts = currentOpts
}
}
}
export interface ErrorsTextOptions {
separator? : string
dataVar? : string
}
function checkOptions (
this : Ajv ,
checkOpts : OptionsInfo < RemovedOptions | DeprecatedOptions > ,
options : Options & RemovedOptions ,
msg : string ,
log : "warn" | "error" = "error"
) : void {
for ( const key in checkOpts ) {
const opt = key as keyof typeof checkOpts
if ( opt in options ) this . logger [ log ] ( ` ${ msg } : option ${ key } . ${ checkOpts [ opt ] } ` )
}
}
function getSchEnv ( this : Ajv , keyRef : string ) : SchemaEnv | string | undefined {
keyRef = normalizeId ( keyRef ) // TODO tests fail without this line
return this . schemas [ keyRef ] || this . refs [ keyRef ]
}
function addInitialSchemas ( this : Ajv ) : void {
const optsSchemas = this . opts . schemas
if ( ! optsSchemas ) return
if ( Array . isArray ( optsSchemas ) ) this . addSchema ( optsSchemas )
else for ( const key in optsSchemas ) this . addSchema ( optsSchemas [ key ] as AnySchema , key )
}
function addInitialFormats ( this : Ajv ) : void {
for ( const name in this . opts . formats ) {
const format = this . opts . formats [ name ]
if ( format ) this . addFormat ( name , format )
}
}
function addInitialKeywords (
this : Ajv ,
defs : Vocabulary | { [ K in string ] ? : KeywordDefinition }
) : void {
if ( Array . isArray ( defs ) ) {
this . addVocabulary ( defs )
return
}
this . logger . warn ( "keywords option as map is deprecated, pass array" )
for ( const keyword in defs ) {
const def = defs [ keyword ] as KeywordDefinition
if ( ! def . keyword ) def . keyword = keyword
this . addKeyword ( def )
}
}
function getMetaSchemaOptions ( this : Ajv ) : InstanceOptions {
const metaOpts = { . . . this . opts }
for ( const opt of META_IGNORE_OPTIONS ) delete metaOpts [ opt ]
return metaOpts
}
const noLogs = { log() { } , warn() { } , error() { } }
function getLogger ( logger? : Partial < Logger > | false ) : Logger {
if ( logger === false ) return noLogs
if ( logger === undefined ) return console
if ( logger . log && logger . warn && logger . error ) return logger as Logger
throw new Error ( "logger must implement log, warn and error methods" )
}
const KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i
function checkKeyword ( this : Ajv , keyword : string | string [ ] , def? : KeywordDefinition ) : void {
const { RULES } = this
eachItem ( keyword , ( kwd ) = > {
if ( RULES . keywords [ kwd ] ) throw new Error ( ` Keyword ${ kwd } is already defined ` )
if ( ! KEYWORD_NAME . test ( kwd ) ) throw new Error ( ` Keyword ${ kwd } has invalid name ` )
} )
if ( ! def ) return
if ( def . $data && ! ( "code" in def || "validate" in def ) ) {
throw new Error ( '$data keyword must have "code" or "validate" function' )
}
}
function addRule (
this : Ajv ,
keyword : string ,
definition? : AddedKeywordDefinition ,
dataType? : JSONType
) : void {
const post = definition ? . post
if ( dataType && post ) throw new Error ( 'keyword with "post" flag cannot have "type"' )
const { RULES } = this
let ruleGroup = post ? RULES.post : RULES.rules.find ( ( { type : t } ) = > t === dataType )
if ( ! ruleGroup ) {
ruleGroup = { type : dataType , rules : [ ] }
RULES . rules . push ( ruleGroup )
}
RULES . keywords [ keyword ] = true
if ( ! definition ) return
const rule : Rule = {
keyword ,
definition : {
. . . definition ,
type : getJSONTypes ( definition . type ) ,
schemaType : getJSONTypes ( definition . schemaType ) ,
} ,
}
if ( definition . before ) addBeforeRule . call ( this , ruleGroup , rule , definition . before )
else ruleGroup . rules . push ( rule )
RULES . all [ keyword ] = rule
definition . implements ? . forEach ( ( kwd ) = > this . addKeyword ( kwd ) )
}
function addBeforeRule ( this : Ajv , ruleGroup : RuleGroup , rule : Rule , before : string ) : void {
const i = ruleGroup . rules . findIndex ( ( _rule ) = > _rule . keyword === before )
if ( i >= 0 ) {
ruleGroup . rules . splice ( i , 0 , rule )
} else {
ruleGroup . rules . push ( rule )
this . logger . warn ( ` rule ${ before } is not defined ` )
}
}
function keywordMetaschema ( this : Ajv , def : KeywordDefinition ) : void {
let { metaSchema } = def
if ( metaSchema === undefined ) return
if ( def . $data && this . opts . $data ) metaSchema = schemaOrData ( metaSchema )
def . validateSchema = this . compile ( metaSchema , true )
}
const $dataRef = {
$ref : "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#" ,
}
function schemaOrData ( schema : AnySchema ) : AnySchemaObject {
return { anyOf : [ schema , $dataRef ] }
}