massive update, probably broken
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / table / node_modules / ajv / lib / core.ts
1 export {
2   Format,
3   FormatDefinition,
4   AsyncFormatDefinition,
5   KeywordDefinition,
6   KeywordErrorDefinition,
7   CodeKeywordDefinition,
8   MacroKeywordDefinition,
9   FuncKeywordDefinition,
10   Vocabulary,
11   Schema,
12   SchemaObject,
13   AnySchemaObject,
14   AsyncSchema,
15   AnySchema,
16   ValidateFunction,
17   AsyncValidateFunction,
18   AnyValidateFunction,
19   ErrorObject,
20   ErrorNoParams,
21 } from "./types"
22
23 export {SchemaCxt, SchemaObjCxt} from "./compile"
24 export interface Plugin<Opts> {
25   (ajv: Ajv, options?: Opts): Ajv
26   [prop: string]: any
27 }
28
29 export {KeywordCxt} from "./compile/validate"
30 export {DefinedError} from "./vocabularies/errors"
31 export {JSONType} from "./compile/rules"
32 export {JSONSchemaType} from "./types/json-schema"
33 export {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
34 export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"
35
36 import type {
37   Schema,
38   AnySchema,
39   AnySchemaObject,
40   SchemaObject,
41   AsyncSchema,
42   Vocabulary,
43   KeywordDefinition,
44   AddedKeywordDefinition,
45   AnyValidateFunction,
46   ValidateFunction,
47   AsyncValidateFunction,
48   ErrorObject,
49   Format,
50   AddedFormat,
51   RegExpEngine,
52 } from "./types"
53 import type {JSONSchemaType} from "./types/json-schema"
54 import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
55 import ValidationError from "./runtime/validation_error"
56 import MissingRefError from "./compile/ref_error"
57 import {getRules, ValidationRules, Rule, RuleGroup, JSONType} from "./compile/rules"
58 import {SchemaEnv, compileSchema, resolveSchema} from "./compile"
59 import {Code, ValueScope} from "./compile/codegen"
60 import {normalizeId, getSchemaRefs} from "./compile/resolve"
61 import {getJSONTypes} from "./compile/validate/dataType"
62 import {eachItem} from "./compile/util"
63
64 import * as $dataRefSchema from "./refs/data.json"
65
66 const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags)
67 defaultRegExp.code = "new RegExp"
68
69 const META_IGNORE_OPTIONS: (keyof Options)[] = ["removeAdditional", "useDefaults", "coerceTypes"]
70 const EXT_SCOPE_NAMES = new Set([
71   "validate",
72   "serialize",
73   "parse",
74   "wrapper",
75   "root",
76   "schema",
77   "keyword",
78   "pattern",
79   "formats",
80   "validate$data",
81   "func",
82   "obj",
83   "Error",
84 ])
85
86 export type Options = CurrentOptions & DeprecatedOptions
87
88 export interface CurrentOptions {
89   // strict mode options (NEW)
90   strict?: boolean | "log"
91   strictSchema?: boolean | "log"
92   strictNumbers?: boolean | "log"
93   strictTypes?: boolean | "log"
94   strictTuples?: boolean | "log"
95   strictRequired?: boolean | "log"
96   allowMatchingProperties?: boolean // disables a strict mode restriction
97   allowUnionTypes?: boolean
98   validateFormats?: boolean
99   // validation and reporting options:
100   $data?: boolean
101   allErrors?: boolean
102   verbose?: boolean
103   discriminator?: boolean
104   unicodeRegExp?: boolean
105   timestamp?: "string" | "date" // JTD only
106   parseDate?: boolean // JTD only
107   allowDate?: boolean // JTD only
108   $comment?:
109     | true
110     | ((comment: string, schemaPath?: string, rootSchema?: AnySchemaObject) => unknown)
111   formats?: {[Name in string]?: Format}
112   keywords?: Vocabulary
113   schemas?: AnySchema[] | {[Key in string]?: AnySchema}
114   logger?: Logger | false
115   loadSchema?: (uri: string) => Promise<AnySchemaObject>
116   // options to modify validated data:
117   removeAdditional?: boolean | "all" | "failing"
118   useDefaults?: boolean | "empty"
119   coerceTypes?: boolean | "array"
120   // advanced options:
121   next?: boolean // NEW
122   unevaluated?: boolean // NEW
123   dynamicRef?: boolean // NEW
124   schemaId?: "id" | "$id"
125   jtd?: boolean // NEW
126   meta?: SchemaObject | boolean
127   defaultMeta?: string | AnySchemaObject
128   validateSchema?: boolean | "log"
129   addUsedSchema?: boolean
130   inlineRefs?: boolean | number
131   passContext?: boolean
132   loopRequired?: number
133   loopEnum?: number // NEW
134   ownProperties?: boolean
135   multipleOfPrecision?: number
136   int32range?: boolean // JTD only
137   messages?: boolean
138   code?: CodeOptions // NEW
139 }
140
141 export interface CodeOptions {
142   es5?: boolean
143   lines?: boolean
144   optimize?: boolean | number
145   formats?: Code // code to require (or construct) map of available formats - for standalone code
146   source?: boolean
147   process?: (code: string, schema?: SchemaEnv) => string
148   regExp?: RegExpEngine
149 }
150
151 interface InstanceCodeOptions extends CodeOptions {
152   regExp: RegExpEngine
153   optimize: number
154 }
155
156 interface DeprecatedOptions {
157   /** @deprecated */
158   ignoreKeywordsWithRef?: boolean
159   /** @deprecated */
160   jsPropertySyntax?: boolean // added instead of jsonPointers
161   /** @deprecated */
162   unicode?: boolean
163 }
164
165 interface RemovedOptions {
166   format?: boolean
167   errorDataPath?: "object" | "property"
168   nullable?: boolean // "nullable" keyword is supported by default
169   jsonPointers?: boolean
170   extendRefs?: true | "ignore" | "fail"
171   missingRefs?: true | "ignore" | "fail"
172   processCode?: (code: string, schema?: SchemaEnv) => string
173   sourceCode?: boolean
174   strictDefaults?: boolean
175   strictKeywords?: boolean
176   uniqueItems?: boolean
177   unknownFormats?: true | string[] | "ignore"
178   cache?: any
179   serialize?: (schema: AnySchema) => unknown
180   ajvErrors?: boolean
181 }
182
183 type OptionsInfo<T extends RemovedOptions | DeprecatedOptions> = {
184   [K in keyof T]-?: string | undefined
185 }
186
187 const removedOptions: OptionsInfo<RemovedOptions> = {
188   errorDataPath: "",
189   format: "`validateFormats: false` can be used instead.",
190   nullable: '"nullable" keyword is supported by default.',
191   jsonPointers: "Deprecated jsPropertySyntax can be used instead.",
192   extendRefs: "Deprecated ignoreKeywordsWithRef can be used instead.",
193   missingRefs: "Pass empty schema with $id that should be ignored to ajv.addSchema.",
194   processCode: "Use option `code: {process: (code, schemaEnv: object) => string}`",
195   sourceCode: "Use option `code: {source: true}`",
196   strictDefaults: "It is default now, see option `strict`.",
197   strictKeywords: "It is default now, see option `strict`.",
198   uniqueItems: '"uniqueItems" keyword is always validated.',
199   unknownFormats: "Disable strict mode or pass `true` to `ajv.addFormat` (or `formats` option).",
200   cache: "Map is used as cache, schema object as key.",
201   serialize: "Map is used as cache, schema object as key.",
202   ajvErrors: "It is default now.",
203 }
204
205 const deprecatedOptions: OptionsInfo<DeprecatedOptions> = {
206   ignoreKeywordsWithRef: "",
207   jsPropertySyntax: "",
208   unicode: '"minLength"/"maxLength" account for unicode characters by default.',
209 }
210
211 type RequiredInstanceOptions = {
212   [K in
213     | "strictSchema"
214     | "strictNumbers"
215     | "strictTypes"
216     | "strictTuples"
217     | "strictRequired"
218     | "inlineRefs"
219     | "loopRequired"
220     | "loopEnum"
221     | "meta"
222     | "messages"
223     | "schemaId"
224     | "addUsedSchema"
225     | "validateSchema"
226     | "validateFormats"
227     | "int32range"
228     | "unicodeRegExp"]: NonNullable<Options[K]>
229 } & {code: InstanceCodeOptions}
230
231 export type InstanceOptions = Options & RequiredInstanceOptions
232
233 const MAX_EXPRESSION = 200
234
235 // eslint-disable-next-line complexity
236 function requiredOptions(o: Options): RequiredInstanceOptions {
237   const s = o.strict
238   const _optz = o.code?.optimize
239   const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0
240   const regExp = o.code?.regExp ?? defaultRegExp
241   return {
242     strictSchema: o.strictSchema ?? s ?? true,
243     strictNumbers: o.strictNumbers ?? s ?? true,
244     strictTypes: o.strictTypes ?? s ?? "log",
245     strictTuples: o.strictTuples ?? s ?? "log",
246     strictRequired: o.strictRequired ?? s ?? false,
247     code: o.code ? {...o.code, optimize, regExp} : {optimize, regExp},
248     loopRequired: o.loopRequired ?? MAX_EXPRESSION,
249     loopEnum: o.loopEnum ?? MAX_EXPRESSION,
250     meta: o.meta ?? true,
251     messages: o.messages ?? true,
252     inlineRefs: o.inlineRefs ?? true,
253     schemaId: o.schemaId ?? "$id",
254     addUsedSchema: o.addUsedSchema ?? true,
255     validateSchema: o.validateSchema ?? true,
256     validateFormats: o.validateFormats ?? true,
257     unicodeRegExp: o.unicodeRegExp ?? true,
258     int32range: o.int32range ?? true,
259   }
260 }
261
262 export interface Logger {
263   log(...args: unknown[]): unknown
264   warn(...args: unknown[]): unknown
265   error(...args: unknown[]): unknown
266 }
267
268 export default class Ajv {
269   opts: InstanceOptions
270   errors?: ErrorObject[] | null // errors from the last validation
271   logger: Logger
272   // shared external scope values for compiled functions
273   readonly scope: ValueScope
274   readonly schemas: {[Key in string]?: SchemaEnv} = {}
275   readonly refs: {[Ref in string]?: SchemaEnv | string} = {}
276   readonly formats: {[Name in string]?: AddedFormat} = {}
277   readonly RULES: ValidationRules
278   readonly _compilations: Set<SchemaEnv> = new Set()
279   private readonly _loading: {[Ref in string]?: Promise<AnySchemaObject>} = {}
280   private readonly _cache: Map<AnySchema, SchemaEnv> = new Map()
281   private readonly _metaOpts: InstanceOptions
282
283   static ValidationError = ValidationError
284   static MissingRefError = MissingRefError
285
286   constructor(opts: Options = {}) {
287     opts = this.opts = {...opts, ...requiredOptions(opts)}
288     const {es5, lines} = this.opts.code
289
290     this.scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES, es5, lines})
291     this.logger = getLogger(opts.logger)
292     const formatOpt = opts.validateFormats
293     opts.validateFormats = false
294
295     this.RULES = getRules()
296     checkOptions.call(this, removedOptions, opts, "NOT SUPPORTED")
297     checkOptions.call(this, deprecatedOptions, opts, "DEPRECATED", "warn")
298     this._metaOpts = getMetaSchemaOptions.call(this)
299
300     if (opts.formats) addInitialFormats.call(this)
301     this._addVocabularies()
302     this._addDefaultMetaSchema()
303     if (opts.keywords) addInitialKeywords.call(this, opts.keywords)
304     if (typeof opts.meta == "object") this.addMetaSchema(opts.meta)
305     addInitialSchemas.call(this)
306     opts.validateFormats = formatOpt
307   }
308
309   _addVocabularies(): void {
310     this.addKeyword("$async")
311   }
312
313   _addDefaultMetaSchema(): void {
314     const {$data, meta, schemaId} = this.opts
315     let _dataRefSchema: SchemaObject = $dataRefSchema
316     if (schemaId === "id") {
317       _dataRefSchema = {...$dataRefSchema}
318       _dataRefSchema.id = _dataRefSchema.$id
319       delete _dataRefSchema.$id
320     }
321     if (meta && $data) this.addMetaSchema(_dataRefSchema, _dataRefSchema[schemaId], false)
322   }
323
324   defaultMeta(): string | AnySchemaObject | undefined {
325     const {meta, schemaId} = this.opts
326     return (this.opts.defaultMeta = typeof meta == "object" ? meta[schemaId] || meta : undefined)
327   }
328
329   // Validate data using schema
330   // AnySchema will be compiled and cached using schema itself as a key for Map
331   validate(schema: Schema | string, data: unknown): boolean
332   validate(schemaKeyRef: AnySchema | string, data: unknown): boolean | Promise<unknown>
333   validate<T>(schema: Schema | JSONSchemaType<T> | string, data: unknown): data is T
334   // Separated for type inference to work
335   // eslint-disable-next-line @typescript-eslint/unified-signatures
336   validate<T>(schema: JTDSchemaType<T>, data: unknown): data is T
337   // This overload is only intended for typescript inference, the first
338   // argument prevents manual type annotation from matching this overload
339   validate<N extends never, T extends SomeJTDSchemaType>(
340     schema: T,
341     data: unknown
342   ): data is JTDDataType<T>
343   validate<T>(schema: AsyncSchema, data: unknown | T): Promise<T>
344   validate<T>(schemaKeyRef: AnySchema | string, data: unknown): data is T | Promise<T>
345   validate<T>(
346     schemaKeyRef: AnySchema | string, // key, ref or schema object
347     data: unknown | T // to be validated
348   ): boolean | Promise<T> {
349     let v: AnyValidateFunction | undefined
350     if (typeof schemaKeyRef == "string") {
351       v = this.getSchema<T>(schemaKeyRef)
352       if (!v) throw new Error(`no schema with key or ref "${schemaKeyRef}"`)
353     } else {
354       v = this.compile<T>(schemaKeyRef)
355     }
356
357     const valid = v(data)
358     if (!("$async" in v)) this.errors = v.errors
359     return valid
360   }
361
362   // Create validation function for passed schema
363   // _meta: true if schema is a meta-schema. Used internally to compile meta schemas of user-defined keywords.
364   compile<T = unknown>(schema: Schema | JSONSchemaType<T>, _meta?: boolean): ValidateFunction<T>
365   // Separated for type inference to work
366   // eslint-disable-next-line @typescript-eslint/unified-signatures
367   compile<T = unknown>(schema: JTDSchemaType<T>, _meta?: boolean): ValidateFunction<T>
368   // This overload is only intended for typescript inference, the first
369   // argument prevents manual type annotation from matching this overload
370   compile<N extends never, T extends SomeJTDSchemaType>(
371     schema: T,
372     _meta?: boolean
373   ): ValidateFunction<JTDDataType<T>>
374   compile<T = unknown>(schema: AsyncSchema, _meta?: boolean): AsyncValidateFunction<T>
375   compile<T = unknown>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T>
376   compile<T = unknown>(schema: AnySchema, _meta?: boolean): AnyValidateFunction<T> {
377     const sch = this._addSchema(schema, _meta)
378     return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction<T>
379   }
380
381   // Creates validating function for passed schema with asynchronous loading of missing schemas.
382   // `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema.
383   // TODO allow passing schema URI
384   // meta - optional true to compile meta-schema
385   compileAsync<T = unknown>(
386     schema: SchemaObject | JSONSchemaType<T>,
387     _meta?: boolean
388   ): Promise<ValidateFunction<T>>
389   // Separated for type inference to work
390   // eslint-disable-next-line @typescript-eslint/unified-signatures
391   compileAsync<T = unknown>(schema: JTDSchemaType<T>, _meta?: boolean): Promise<ValidateFunction<T>>
392   compileAsync<T = unknown>(schema: AsyncSchema, meta?: boolean): Promise<AsyncValidateFunction<T>>
393   // eslint-disable-next-line @typescript-eslint/unified-signatures
394   compileAsync<T = unknown>(
395     schema: AnySchemaObject,
396     meta?: boolean
397   ): Promise<AnyValidateFunction<T>>
398   compileAsync<T = unknown>(
399     schema: AnySchemaObject,
400     meta?: boolean
401   ): Promise<AnyValidateFunction<T>> {
402     if (typeof this.opts.loadSchema != "function") {
403       throw new Error("options.loadSchema should be a function")
404     }
405     const {loadSchema} = this.opts
406     return runCompileAsync.call(this, schema, meta)
407
408     async function runCompileAsync(
409       this: Ajv,
410       _schema: AnySchemaObject,
411       _meta?: boolean
412     ): Promise<AnyValidateFunction> {
413       await loadMetaSchema.call(this, _schema.$schema)
414       const sch = this._addSchema(_schema, _meta)
415       return sch.validate || _compileAsync.call(this, sch)
416     }
417
418     async function loadMetaSchema(this: Ajv, $ref?: string): Promise<void> {
419       if ($ref && !this.getSchema($ref)) {
420         await runCompileAsync.call(this, {$ref}, true)
421       }
422     }
423
424     async function _compileAsync(this: Ajv, sch: SchemaEnv): Promise<AnyValidateFunction> {
425       try {
426         return this._compileSchemaEnv(sch)
427       } catch (e) {
428         if (!(e instanceof MissingRefError)) throw e
429         checkLoaded.call(this, e)
430         await loadMissingSchema.call(this, e.missingSchema)
431         return _compileAsync.call(this, sch)
432       }
433     }
434
435     function checkLoaded(this: Ajv, {missingSchema: ref, missingRef}: MissingRefError): void {
436       if (this.refs[ref]) {
437         throw new Error(`AnySchema ${ref} is loaded but ${missingRef} cannot be resolved`)
438       }
439     }
440
441     async function loadMissingSchema(this: Ajv, ref: string): Promise<void> {
442       const _schema = await _loadSchema.call(this, ref)
443       if (!this.refs[ref]) await loadMetaSchema.call(this, _schema.$schema)
444       if (!this.refs[ref]) this.addSchema(_schema, ref, meta)
445     }
446
447     async function _loadSchema(this: Ajv, ref: string): Promise<AnySchemaObject> {
448       const p = this._loading[ref]
449       if (p) return p
450       try {
451         return await (this._loading[ref] = loadSchema(ref))
452       } finally {
453         delete this._loading[ref]
454       }
455     }
456   }
457
458   // Adds schema to the instance
459   addSchema(
460     schema: AnySchema | AnySchema[], // If array is passed, `key` will be ignored
461     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`.
462     _meta?: boolean, // true if schema is a meta-schema. Used internally, addMetaSchema should be used instead.
463     _validateSchema = this.opts.validateSchema // false to skip schema validation. Used internally, option validateSchema should be used instead.
464   ): Ajv {
465     if (Array.isArray(schema)) {
466       for (const sch of schema) this.addSchema(sch, undefined, _meta, _validateSchema)
467       return this
468     }
469     let id: string | undefined
470     if (typeof schema === "object") {
471       const {schemaId} = this.opts
472       id = schema[schemaId]
473       if (id !== undefined && typeof id != "string") {
474         throw new Error(`schema ${schemaId} must be string`)
475       }
476     }
477     key = normalizeId(key || id)
478     this._checkUnique(key)
479     this.schemas[key] = this._addSchema(schema, _meta, key, _validateSchema, true)
480     return this
481   }
482
483   // Add schema that will be used to validate other schemas
484   // options in META_IGNORE_OPTIONS are alway set to false
485   addMetaSchema(
486     schema: AnySchemaObject,
487     key?: string, // schema key
488     _validateSchema = this.opts.validateSchema // false to skip schema validation, can be used to override validateSchema option for meta-schema
489   ): Ajv {
490     this.addSchema(schema, key, true, _validateSchema)
491     return this
492   }
493
494   //  Validate schema against its meta-schema
495   validateSchema(schema: AnySchema, throwOrLogError?: boolean): boolean | Promise<unknown> {
496     if (typeof schema == "boolean") return true
497     let $schema: string | AnySchemaObject | undefined
498     $schema = schema.$schema
499     if ($schema !== undefined && typeof $schema != "string") {
500       throw new Error("$schema must be a string")
501     }
502     $schema = $schema || this.opts.defaultMeta || this.defaultMeta()
503     if (!$schema) {
504       this.logger.warn("meta-schema not available")
505       this.errors = null
506       return true
507     }
508     const valid = this.validate($schema, schema)
509     if (!valid && throwOrLogError) {
510       const message = "schema is invalid: " + this.errorsText()
511       if (this.opts.validateSchema === "log") this.logger.error(message)
512       else throw new Error(message)
513     }
514     return valid
515   }
516
517   // Get compiled schema by `key` or `ref`.
518   // (`key` that was passed to `addSchema` or full schema reference - `schema.$id` or resolved id)
519   getSchema<T = unknown>(keyRef: string): AnyValidateFunction<T> | undefined {
520     let sch
521     while (typeof (sch = getSchEnv.call(this, keyRef)) == "string") keyRef = sch
522     if (sch === undefined) {
523       const {schemaId} = this.opts
524       const root = new SchemaEnv({schema: {}, schemaId})
525       sch = resolveSchema.call(this, root, keyRef)
526       if (!sch) return
527       this.refs[keyRef] = sch
528     }
529     return (sch.validate || this._compileSchemaEnv(sch)) as AnyValidateFunction<T> | undefined
530   }
531
532   // Remove cached schema(s).
533   // If no parameter is passed all schemas but meta-schemas are removed.
534   // If RegExp is passed all schemas with key/id matching pattern but meta-schemas are removed.
535   // Even if schema is referenced by other schemas it still can be removed as other schemas have local references.
536   removeSchema(schemaKeyRef?: AnySchema | string | RegExp): Ajv {
537     if (schemaKeyRef instanceof RegExp) {
538       this._removeAllSchemas(this.schemas, schemaKeyRef)
539       this._removeAllSchemas(this.refs, schemaKeyRef)
540       return this
541     }
542     switch (typeof schemaKeyRef) {
543       case "undefined":
544         this._removeAllSchemas(this.schemas)
545         this._removeAllSchemas(this.refs)
546         this._cache.clear()
547         return this
548       case "string": {
549         const sch = getSchEnv.call(this, schemaKeyRef)
550         if (typeof sch == "object") this._cache.delete(sch.schema)
551         delete this.schemas[schemaKeyRef]
552         delete this.refs[schemaKeyRef]
553         return this
554       }
555       case "object": {
556         const cacheKey = schemaKeyRef
557         this._cache.delete(cacheKey)
558         let id = schemaKeyRef[this.opts.schemaId]
559         if (id) {
560           id = normalizeId(id)
561           delete this.schemas[id]
562           delete this.refs[id]
563         }
564         return this
565       }
566       default:
567         throw new Error("ajv.removeSchema: invalid parameter")
568     }
569   }
570
571   // add "vocabulary" - a collection of keywords
572   addVocabulary(definitions: Vocabulary): Ajv {
573     for (const def of definitions) this.addKeyword(def)
574     return this
575   }
576
577   addKeyword(
578     kwdOrDef: string | KeywordDefinition,
579     def?: KeywordDefinition // deprecated
580   ): Ajv {
581     let keyword: string | string[]
582     if (typeof kwdOrDef == "string") {
583       keyword = kwdOrDef
584       if (typeof def == "object") {
585         this.logger.warn("these parameters are deprecated, see docs for addKeyword")
586         def.keyword = keyword
587       }
588     } else if (typeof kwdOrDef == "object" && def === undefined) {
589       def = kwdOrDef
590       keyword = def.keyword
591       if (Array.isArray(keyword) && !keyword.length) {
592         throw new Error("addKeywords: keyword must be string or non-empty array")
593       }
594     } else {
595       throw new Error("invalid addKeywords parameters")
596     }
597
598     checkKeyword.call(this, keyword, def)
599     if (!def) {
600       eachItem(keyword, (kwd) => addRule.call(this, kwd))
601       return this
602     }
603     keywordMetaschema.call(this, def)
604     const definition: AddedKeywordDefinition = {
605       ...def,
606       type: getJSONTypes(def.type),
607       schemaType: getJSONTypes(def.schemaType),
608     }
609     eachItem(
610       keyword,
611       definition.type.length === 0
612         ? (k) => addRule.call(this, k, definition)
613         : (k) => definition.type.forEach((t) => addRule.call(this, k, definition, t))
614     )
615     return this
616   }
617
618   getKeyword(keyword: string): AddedKeywordDefinition | boolean {
619     const rule = this.RULES.all[keyword]
620     return typeof rule == "object" ? rule.definition : !!rule
621   }
622
623   // Remove keyword
624   removeKeyword(keyword: string): Ajv {
625     // TODO return type should be Ajv
626     const {RULES} = this
627     delete RULES.keywords[keyword]
628     delete RULES.all[keyword]
629     for (const group of RULES.rules) {
630       const i = group.rules.findIndex((rule) => rule.keyword === keyword)
631       if (i >= 0) group.rules.splice(i, 1)
632     }
633     return this
634   }
635
636   // Add format
637   addFormat(name: string, format: Format): Ajv {
638     if (typeof format == "string") format = new RegExp(format)
639     this.formats[name] = format
640     return this
641   }
642
643   errorsText(
644     errors: ErrorObject[] | null | undefined = this.errors, // optional array of validation errors
645     {separator = ", ", dataVar = "data"}: ErrorsTextOptions = {} // optional options with properties `separator` and `dataVar`
646   ): string {
647     if (!errors || errors.length === 0) return "No errors"
648     return errors
649       .map((e) => `${dataVar}${e.instancePath} ${e.message}`)
650       .reduce((text, msg) => text + separator + msg)
651   }
652
653   $dataMetaSchema(metaSchema: AnySchemaObject, keywordsJsonPointers: string[]): AnySchemaObject {
654     const rules = this.RULES.all
655     metaSchema = JSON.parse(JSON.stringify(metaSchema))
656     for (const jsonPointer of keywordsJsonPointers) {
657       const segments = jsonPointer.split("/").slice(1) // first segment is an empty string
658       let keywords = metaSchema
659       for (const seg of segments) keywords = keywords[seg] as AnySchemaObject
660
661       for (const key in rules) {
662         const rule = rules[key]
663         if (typeof rule != "object") continue
664         const {$data} = rule.definition
665         const schema = keywords[key] as AnySchemaObject | undefined
666         if ($data && schema) keywords[key] = schemaOrData(schema)
667       }
668     }
669
670     return metaSchema
671   }
672
673   private _removeAllSchemas(schemas: {[Ref in string]?: SchemaEnv | string}, regex?: RegExp): void {
674     for (const keyRef in schemas) {
675       const sch = schemas[keyRef]
676       if (!regex || regex.test(keyRef)) {
677         if (typeof sch == "string") {
678           delete schemas[keyRef]
679         } else if (sch && !sch.meta) {
680           this._cache.delete(sch.schema)
681           delete schemas[keyRef]
682         }
683       }
684     }
685   }
686
687   _addSchema(
688     schema: AnySchema,
689     meta?: boolean,
690     baseId?: string,
691     validateSchema = this.opts.validateSchema,
692     addSchema = this.opts.addUsedSchema
693   ): SchemaEnv {
694     let id: string | undefined
695     const {schemaId} = this.opts
696     if (typeof schema == "object") {
697       id = schema[schemaId]
698     } else {
699       if (this.opts.jtd) throw new Error("schema must be object")
700       else if (typeof schema != "boolean") throw new Error("schema must be object or boolean")
701     }
702     let sch = this._cache.get(schema)
703     if (sch !== undefined) return sch
704
705     baseId = normalizeId(id || baseId)
706     const localRefs = getSchemaRefs.call(this, schema, baseId)
707     sch = new SchemaEnv({schema, schemaId, meta, baseId, localRefs})
708     this._cache.set(sch.schema, sch)
709     if (addSchema && !baseId.startsWith("#")) {
710       // TODO atm it is allowed to overwrite schemas without id (instead of not adding them)
711       if (baseId) this._checkUnique(baseId)
712       this.refs[baseId] = sch
713     }
714     if (validateSchema) this.validateSchema(schema, true)
715     return sch
716   }
717
718   private _checkUnique(id: string): void {
719     if (this.schemas[id] || this.refs[id]) {
720       throw new Error(`schema with key or id "${id}" already exists`)
721     }
722   }
723
724   private _compileSchemaEnv(sch: SchemaEnv): AnyValidateFunction {
725     if (sch.meta) this._compileMetaSchema(sch)
726     else compileSchema.call(this, sch)
727
728     /* istanbul ignore if */
729     if (!sch.validate) throw new Error("ajv implementation error")
730     return sch.validate
731   }
732
733   private _compileMetaSchema(sch: SchemaEnv): void {
734     const currentOpts = this.opts
735     this.opts = this._metaOpts
736     try {
737       compileSchema.call(this, sch)
738     } finally {
739       this.opts = currentOpts
740     }
741   }
742 }
743
744 export interface ErrorsTextOptions {
745   separator?: string
746   dataVar?: string
747 }
748
749 function checkOptions(
750   this: Ajv,
751   checkOpts: OptionsInfo<RemovedOptions | DeprecatedOptions>,
752   options: Options & RemovedOptions,
753   msg: string,
754   log: "warn" | "error" = "error"
755 ): void {
756   for (const key in checkOpts) {
757     const opt = key as keyof typeof checkOpts
758     if (opt in options) this.logger[log](`${msg}: option ${key}. ${checkOpts[opt]}`)
759   }
760 }
761
762 function getSchEnv(this: Ajv, keyRef: string): SchemaEnv | string | undefined {
763   keyRef = normalizeId(keyRef) // TODO tests fail without this line
764   return this.schemas[keyRef] || this.refs[keyRef]
765 }
766
767 function addInitialSchemas(this: Ajv): void {
768   const optsSchemas = this.opts.schemas
769   if (!optsSchemas) return
770   if (Array.isArray(optsSchemas)) this.addSchema(optsSchemas)
771   else for (const key in optsSchemas) this.addSchema(optsSchemas[key] as AnySchema, key)
772 }
773
774 function addInitialFormats(this: Ajv): void {
775   for (const name in this.opts.formats) {
776     const format = this.opts.formats[name]
777     if (format) this.addFormat(name, format)
778   }
779 }
780
781 function addInitialKeywords(
782   this: Ajv,
783   defs: Vocabulary | {[K in string]?: KeywordDefinition}
784 ): void {
785   if (Array.isArray(defs)) {
786     this.addVocabulary(defs)
787     return
788   }
789   this.logger.warn("keywords option as map is deprecated, pass array")
790   for (const keyword in defs) {
791     const def = defs[keyword] as KeywordDefinition
792     if (!def.keyword) def.keyword = keyword
793     this.addKeyword(def)
794   }
795 }
796
797 function getMetaSchemaOptions(this: Ajv): InstanceOptions {
798   const metaOpts = {...this.opts}
799   for (const opt of META_IGNORE_OPTIONS) delete metaOpts[opt]
800   return metaOpts
801 }
802
803 const noLogs = {log() {}, warn() {}, error() {}}
804
805 function getLogger(logger?: Partial<Logger> | false): Logger {
806   if (logger === false) return noLogs
807   if (logger === undefined) return console
808   if (logger.log && logger.warn && logger.error) return logger as Logger
809   throw new Error("logger must implement log, warn and error methods")
810 }
811
812 const KEYWORD_NAME = /^[a-z_$][a-z0-9_$:-]*$/i
813
814 function checkKeyword(this: Ajv, keyword: string | string[], def?: KeywordDefinition): void {
815   const {RULES} = this
816   eachItem(keyword, (kwd) => {
817     if (RULES.keywords[kwd]) throw new Error(`Keyword ${kwd} is already defined`)
818     if (!KEYWORD_NAME.test(kwd)) throw new Error(`Keyword ${kwd} has invalid name`)
819   })
820   if (!def) return
821   if (def.$data && !("code" in def || "validate" in def)) {
822     throw new Error('$data keyword must have "code" or "validate" function')
823   }
824 }
825
826 function addRule(
827   this: Ajv,
828   keyword: string,
829   definition?: AddedKeywordDefinition,
830   dataType?: JSONType
831 ): void {
832   const post = definition?.post
833   if (dataType && post) throw new Error('keyword with "post" flag cannot have "type"')
834   const {RULES} = this
835   let ruleGroup = post ? RULES.post : RULES.rules.find(({type: t}) => t === dataType)
836   if (!ruleGroup) {
837     ruleGroup = {type: dataType, rules: []}
838     RULES.rules.push(ruleGroup)
839   }
840   RULES.keywords[keyword] = true
841   if (!definition) return
842
843   const rule: Rule = {
844     keyword,
845     definition: {
846       ...definition,
847       type: getJSONTypes(definition.type),
848       schemaType: getJSONTypes(definition.schemaType),
849     },
850   }
851   if (definition.before) addBeforeRule.call(this, ruleGroup, rule, definition.before)
852   else ruleGroup.rules.push(rule)
853   RULES.all[keyword] = rule
854   definition.implements?.forEach((kwd) => this.addKeyword(kwd))
855 }
856
857 function addBeforeRule(this: Ajv, ruleGroup: RuleGroup, rule: Rule, before: string): void {
858   const i = ruleGroup.rules.findIndex((_rule) => _rule.keyword === before)
859   if (i >= 0) {
860     ruleGroup.rules.splice(i, 0, rule)
861   } else {
862     ruleGroup.rules.push(rule)
863     this.logger.warn(`rule ${before} is not defined`)
864   }
865 }
866
867 function keywordMetaschema(this: Ajv, def: KeywordDefinition): void {
868   let {metaSchema} = def
869   if (metaSchema === undefined) return
870   if (def.$data && this.opts.$data) metaSchema = schemaOrData(metaSchema)
871   def.validateSchema = this.compile(metaSchema, true)
872 }
873
874 const $dataRef = {
875   $ref: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#",
876 }
877
878 function schemaOrData(schema: AnySchema): AnySchemaObject {
879   return {anyOf: [schema, $dataRef]}
880 }