code:
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
}

export {KeywordCxt} from "./compile/validate"
export {DefinedError} from "./vocabularies/errors"
export {JSONType} from "./compile/rules"
export {JSONSchemaType} from "./types/json-schema"
export {JTDSchemaType, SomeJTDSchemaType, JTDDataType} 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,
  RegExpEngine,
  UriResolver,
} from "./types"
import type {JSONSchemaType} from "./types/json-schema"
import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
import ValidationError from "./runtime/validation_error"
import MissingRefError from "./compile/ref_error"
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"

import DefaultUriResolver from "./runtime/uri"

const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags)
defaultRegExp.code = "new RegExp"

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"
  strictSchema?: boolean | "log"
  strictNumbers?: 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
  discriminator?: boolean
  unicodeRegExp?: boolean
  timestamp?: "string" | "date" // JTD only
  parseDate?: boolean // JTD only
  allowDate?: boolean // JTD only
  $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
  schemaId?: "id" | "$id"
  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
  int32range?: boolean // JTD only
  messages?: boolean
  code?: CodeOptions // NEW
  uriResolver?: UriResolver
}

export interface CodeOptions {
  es5?: boolean
  esm?: 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
  regExp?: RegExpEngine
}

interface InstanceCodeOptions extends CodeOptions {
  regExp: RegExpEngine
  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
  strictDefaults?: boolean
  strictKeywords?: boolean
  uniqueItems?: boolean
  unknownFormats?: true | string[] | "ignore"
  cache?: any
  serialize?: (schema: AnySchema) => unknown
  ajvErrors?: boolean
}

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}`",
  strictDefaults: "It is default now, see option `strict`.",
  strictKeywords: "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.",
  ajvErrors: "It is default now.",
}

const deprecatedOptions: OptionsInfo<DeprecatedOptions> = {
  ignoreKeywordsWithRef: "",
  jsPropertySyntax: "",
  unicode: '"minLength"/"maxLength" account for unicode characters by default.',
}

type RequiredInstanceOptions = {
  [K in
    | "strictSchema"
    | "strictNumbers"
    | "strictTypes"
    | "strictTuples"
    | "strictRequired"
    | "inlineRefs"
    | "loopRequired"
    | "loopEnum"
    | "meta"
    | "messages"
    | "schemaId"
    | "addUsedSchema"
    | "validateSchema"
    | "validateFormats"
    | "int32range"
    | "unicodeRegExp"
    | "uriResolver"]: NonNullable<Options[K]>
} & {code: InstanceCodeOptions}

export type InstanceOptions = Options & RequiredInstanceOptions

const MAX_EXPRESSION = 200

// eslint-disable-next-line complexity
function requiredOptions(o: Options): RequiredInstanceOptions {
  const s = o.strict
  const _optz = o.code?.optimize
  const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0
  const regExp = o.code?.regExp ?? defaultRegExp
  const uriResolver = o.uriResolver ?? DefaultUriResolver
  return {
    strictSchema: o.strictSchema ?? s ?? true,
    strictNumbers: o.strictNumbers ?? s ?? true,
    strictTypes: o.strictTypes ?? s ?? "log",
    strictTuples: o.strictTuples ?? s ?? "log",
    strictRequired: o.strictRequired ?? s ?? false,
    code: o.code ? {...o.code, optimize, regExp} : {optimize, regExp},
    loopRequired: o.loopRequired ?? MAX_EXPRESSION,
    loopEnum: o.loopEnum ?? MAX_EXPRESSION,
    meta: o.meta ?? true,
    messages: o.messages ?? true,
    inlineRefs: o.inlineRefs ?? true,
    schemaId: o.schemaId ?? "$id",
    addUsedSchema: o.addUsedSchema ?? true,
    validateSchema: o.validateSchema ?? true,
    validateFormats: o.validateFormats ?? true,
    unicodeRegExp: o.unicodeRegExp ?? true,
    int32range: o.int32range ?? true,
    uriResolver: uriResolver,
  }
}

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, schemaId} = this.opts
    let _dataRefSchema: SchemaObject = $dataRefSchema
    if (schemaId === "id") {
      _dataRefSchema = {...$dataRefSchema}
      _dataRefSchema.id = _dataRefSchema.$id
      delete _dataRefSchema.$id
    }
    if (meta && $data) this.addMetaSchema(_dataRefSchema, _dataRefSchema[schemaId], false)
  }

  defaultMeta(): string | AnySchemaObject | undefined {
    const {meta, schemaId} = this.opts
    return (this.opts.defaultMeta = typeof meta == "object" ? meta[schemaId] || 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
  // This overload is only intended for typescript inference, the first
  // argument prevents manual type annotation from matching this overload
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  validate<N extends never, T extends SomeJTDSchemaType>(
    schema: T,
    data: unknown
  ): data is JTDDataType<T>
  // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
  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
    // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
    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>
  // This overload is only intended for typescript inference, the first
  // argument prevents manual type annotation from matching this overload
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  compile<N extends never, T extends SomeJTDSchemaType>(
    schema: T,
    _meta?: boolean
  ): ValidateFunction<JTDDataType<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") {
      const {schemaId} = this.opts
      id = schema[schemaId]
      if (id !== undefined && typeof id != "string") {
        throw new Error(`schema ${schemaId} must be string`)
      }
    }
    key = normalizeId(key || id)
    this._checkUnique(key)
    this.schemas[key] = this._addSchema(schema, _meta, key, _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 {schemaId} = this.opts
      const root = new SchemaEnv({schema: {}, schemaId})
      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[this.opts.schemaId]
        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.instancePath} ${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,
    baseId?: string,
    validateSchema = this.opts.validateSchema,
    addSchema = this.opts.addUsedSchema
  ): SchemaEnv {
    let id: string | undefined
    const {schemaId} = this.opts
    if (typeof schema == "object") {
      id = schema[schemaId]
    } else {
      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

    baseId = normalizeId(id || baseId)
    const localRefs = getSchemaRefs.call(this, schema, baseId)
    sch = new SchemaEnv({schema, schemaId, meta, baseId, localRefs})
    this._cache.set(sch.schema, sch)
    if (addSchema && !baseId.startsWith("#")) {
      // TODO atm it is allowed to overwrite schemas without id (instead of not adding them)
      if (baseId) this._checkUnique(baseId)
      this.refs[baseId] = 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]}
}


code:
import type {AnySchemaObject, SchemaObject, JTDParser} from "./types"
import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
import AjvCore, {CurrentOptions} from "./core"
import jtdVocabulary from "./vocabularies/jtd"
import jtdMetaSchema from "./refs/jtd-schema"
import compileSerializer from "./compile/jtd/serialize"
import compileParser from "./compile/jtd/parse"
import {SchemaEnv} from "./compile"

const META_SCHEMA_ID = "JTD-meta-schema"

type JTDOptions = CurrentOptions & {
  // strict mode options not supported with JTD:
  strict?: never
  allowMatchingProperties?: never
  allowUnionTypes?: never
  validateFormats?: never
  // validation and reporting options not supported with JTD:
  $data?: never
  verbose?: boolean
  $comment?: never
  formats?: never
  loadSchema?: never
  // options to modify validated data:
  useDefaults?: never
  coerceTypes?: never
  // advanced options:
  next?: never
  unevaluated?: never
  dynamicRef?: never
  meta?: boolean
  defaultMeta?: never
  inlineRefs?: boolean
  loopRequired?: never
  multipleOfPrecision?: never
}

export class Ajv extends AjvCore {
  constructor(opts: JTDOptions = {}) {
    super({
      ...opts,
      jtd: true,
    })
  }

  _addVocabularies(): void {
    super._addVocabularies()
    this.addVocabulary(jtdVocabulary)
  }

  _addDefaultMetaSchema(): void {
    super._addDefaultMetaSchema()
    if (!this.opts.meta) return
    this.addMetaSchema(jtdMetaSchema, META_SCHEMA_ID, false)
  }

  defaultMeta(): string | AnySchemaObject | undefined {
    return (this.opts.defaultMeta =
      super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined))
  }

  compileSerializer<T = unknown>(schema: SchemaObject): (data: T) => string
  // Separated for type inference to work
  // eslint-disable-next-line @typescript-eslint/unified-signatures
  compileSerializer<T = unknown>(schema: JTDSchemaType<T>): (data: T) => string
  compileSerializer<T = unknown>(schema: SchemaObject): (data: T) => string {
    const sch = this._addSchema(schema)
    return sch.serialize || this._compileSerializer(sch)
  }

  compileParser<T = unknown>(schema: SchemaObject): JTDParser<T>
  // Separated for type inference to work
  // eslint-disable-next-line @typescript-eslint/unified-signatures
  compileParser<T = unknown>(schema: JTDSchemaType<T>): JTDParser<T>
  compileParser<T = unknown>(schema: SchemaObject): JTDParser<T> {
    const sch = this._addSchema(schema)
    return (sch.parse || this._compileParser(sch)) as JTDParser<T>
  }

  private _compileSerializer<T>(sch: SchemaEnv): (data: T) => string {
    compileSerializer.call(this, sch, (sch.schema as AnySchemaObject).definitions || {})
    /* istanbul ignore if */
    if (!sch.serialize) throw new Error("ajv implementation error")
    return sch.serialize
  }

  private _compileParser(sch: SchemaEnv): JTDParser {
    compileParser.call(this, sch, (sch.schema as AnySchemaObject).definitions || {})
    /* istanbul ignore if */
    if (!sch.parse) throw new Error("ajv implementation error")
    return sch.parse
  }
}

module.exports = exports = Ajv
module.exports.Ajv = Ajv
Object.defineProperty(exports, "__esModule", {value: true})

export default Ajv

export {
  Format,
  FormatDefinition,
  AsyncFormatDefinition,
  KeywordDefinition,
  KeywordErrorDefinition,
  CodeKeywordDefinition,
  MacroKeywordDefinition,
  FuncKeywordDefinition,
  Vocabulary,
  Schema,
  SchemaObject,
  AnySchemaObject,
  AsyncSchema,
  AnySchema,
  ValidateFunction,
  AsyncValidateFunction,
  ErrorObject,
  ErrorNoParams,
  JTDParser,
} from "./types"

export {Plugin, Options, CodeOptions, InstanceOptions, Logger, ErrorsTextOptions} from "./core"
export {SchemaCxt, SchemaObjCxt} from "./compile"
export {KeywordCxt} from "./compile/validate"
export {JTDErrorObject} from "./vocabularies/jtd"
export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen"

export {JTDSchemaType, SomeJTDSchemaType, JTDDataType}
export {JTDOptions}
export {default as ValidationError} from "./runtime/validation_error"
export {default as MissingRefError} from "./compile/ref_error"

license: mit


description: Meta-schema for $data reference (JSON AnySchema extension proposal)

type: object

type: string

format: relative-json-pointer

format: json-pointer


code:
import type Ajv from "../../core"
import type {AnySchemaObject} from "../../types"
import * as metaSchema from "./schema.json"
import * as applicator from "./meta/applicator.json"
import * as content from "./meta/content.json"
import * as core from "./meta/core.json"
import * as format from "./meta/format.json"
import * as metadata from "./meta/meta-data.json"
import * as validation from "./meta/validation.json"

const META_SUPPORT_DATA = ["/properties"]

export default function addMetaSchema2019(this: Ajv, $data?: boolean): Ajv {
  ;[
    metaSchema,
    applicator,
    content,
    core,
    with$data(this, format),
    metadata,
    with$data(this, validation),
  ].forEach((sch) => this.addMetaSchema(sch, undefined, false))
  return this

  function with$data(ajv: Ajv, sch: AnySchemaObject): AnySchemaObject {
    return $data ? ajv.$dataMetaSchema(sch, META_SUPPORT_DATA) : sch
  }
}


title: Applicator vocabulary meta-schema

type: object boolean

$ref: #/$defs/schemaArray

format: regex

type: array


title: Content vocabulary meta-schema


title: Core vocabulary meta-schema

format: uri-reference

$comment: Non-empty fragments not allowed.

format: uri

type: boolean


title: Format vocabulary meta-schema


title: Meta-data vocabulary meta-schema


title: Validation vocabulary meta-schema

type: number

$ref: #/$defs/nonNegativeInteger

$ref: #/$defs/stringArray

$ref: #/$defs/simpleTypes

type: integer

enum: array boolean integer null number object string


title: Core and Validation specifications meta-schema

$ref: meta/core

$ref: meta/applicator

$ref: meta/validation

$ref: meta/meta-data

$ref: meta/format

$ref: meta/content

$comment: While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.

$comment: "dependencies" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to "dependentSchemas" and "dependentRequired"

$ref: meta/validation#/$defs/stringArray


code:
import type Ajv from "../../core"
import type {AnySchemaObject} from "../../types"
import * as metaSchema from "./schema.json"
import * as applicator from "./meta/applicator.json"
import * as unevaluated from "./meta/unevaluated.json"
import * as content from "./meta/content.json"
import * as core from "./meta/core.json"
import * as format from "./meta/format-annotation.json"
import * as metadata from "./meta/meta-data.json"
import * as validation from "./meta/validation.json"

const META_SUPPORT_DATA = ["/properties"]

export default function addMetaSchema2020(this: Ajv, $data?: boolean): Ajv {
  ;[
    metaSchema,
    applicator,
    unevaluated,
    content,
    core,
    with$data(this, format),
    metadata,
    with$data(this, validation),
  ].forEach((sch) => this.addMetaSchema(sch, undefined, false))
  return this

  function with$data(ajv: Ajv, sch: AnySchemaObject): AnySchemaObject {
    return $data ? ajv.$dataMetaSchema(sch, META_SUPPORT_DATA) : sch
  }
}


$dynamicAnchor: meta

title: Applicator vocabulary meta-schema

$dynamicRef: #meta


title: Content vocabulary meta-schema


title: Core vocabulary meta-schema

$ref: #/$defs/uriReferenceString

$ref: #/$defs/uriString

$ref: #/$defs/anchorString


title: Format vocabulary meta-schema for annotation results


title: Meta-data vocabulary meta-schema


title: Unevaluated applicator vocabulary meta-schema


title: Validation vocabulary meta-schema


$ref: meta/unevaluated

$ref: meta/format-annotation

$comment: This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.

$comment: "definitions" has been replaced by "$defs".

$comment: "dependencies" has been split and replaced by "dependentSchemas" and "dependentRequired" in order to serve their differing semantics.

$comment: "$recursiveAnchor" has been replaced by "$dynamicAnchor".

$ref: meta/core#/$defs/anchorString

$comment: "$recursiveRef" has been replaced by "$dynamicRef".

$ref: meta/core#/$defs/uriReferenceString


title: Core schema meta-schema

$ref: #/definitions/nonNegativeInteger

$ref: #/definitions/schemaArray

$ref: #/definitions/stringArray

$ref: #/definitions/simpleTypes


title: Core schema meta-schema


title: Meta-schema for the security assessment of JSON Schemas

description: If a JSON AnySchema fails validation against this meta-schema, it may be unsafe to validate untrusted data

description: prevent slow validation of large property names

description: prevent slow validation of large non-scalar arrays

enum: object array

description: prevent slow pattern matching of large strings

description: prevent slow format validation of large strings


code:
import {SchemaObject} from "../types"

type MetaSchema = (root: boolean) => SchemaObject

const shared: MetaSchema = (root) => {
  const sch: SchemaObject = {
    nullable: {type: "boolean"},
    metadata: {
      optionalProperties: {
        union: {elements: {ref: "schema"}},
      },
      additionalProperties: true,
    },
  }
  if (root) sch.definitions = {values: {ref: "schema"}}
  return sch
}

const emptyForm: MetaSchema = (root) => ({
  optionalProperties: shared(root),
})

const refForm: MetaSchema = (root) => ({
  properties: {
    ref: {type: "string"},
  },
  optionalProperties: shared(root),
})

const typeForm: MetaSchema = (root) => ({
  properties: {
    type: {
      enum: [
        "boolean",
        "timestamp",
        "string",
        "float32",
        "float64",
        "int8",
        "uint8",
        "int16",
        "uint16",
        "int32",
        "uint32",
      ],
    },
  },
  optionalProperties: shared(root),
})

const enumForm: MetaSchema = (root) => ({
  properties: {
    enum: {elements: {type: "string"}},
  },
  optionalProperties: shared(root),
})

const elementsForm: MetaSchema = (root) => ({
  properties: {
    elements: {ref: "schema"},
  },
  optionalProperties: shared(root),
})

const propertiesForm: MetaSchema = (root) => ({
  properties: {
    properties: {values: {ref: "schema"}},
  },
  optionalProperties: {
    optionalProperties: {values: {ref: "schema"}},
    additionalProperties: {type: "boolean"},
    ...shared(root),
  },
})

const optionalPropertiesForm: MetaSchema = (root) => ({
  properties: {
    optionalProperties: {values: {ref: "schema"}},
  },
  optionalProperties: {
    additionalProperties: {type: "boolean"},
    ...shared(root),
  },
})

const discriminatorForm: MetaSchema = (root) => ({
  properties: {
    discriminator: {type: "string"},
    mapping: {
      values: {
        metadata: {
          union: [propertiesForm(false), optionalPropertiesForm(false)],
        },
      },
    },
  },
  optionalProperties: shared(root),
})

const valuesForm: MetaSchema = (root) => ({
  properties: {
    values: {ref: "schema"},
  },
  optionalProperties: shared(root),
})

const schema: MetaSchema = (root) => ({
  metadata: {
    union: [
      emptyForm,
      refForm,
      typeForm,
      enumForm,
      elementsForm,
      propertiesForm,
      optionalPropertiesForm,
      discriminatorForm,
      valuesForm,
    ].map((s) => s(root)),
  },
})

const jtdMetaSchema: SchemaObject = {
  definitions: {
    schema: schema(false),
  },
  ...schema(true),
}

export default jtdMetaSchema


code:
// https://github.com/ajv-validator/ajv/issues/889
import * as equal from "fast-deep-equal"

type Equal = typeof equal & {code: string}
;(equal as Equal).code = 'require("ajv/dist/runtime/equal").default'

export default equal as Equal


code:
const rxParseJson = /position\s(\d+)(?: \(line \d+ column \d+\))?$/

export function parseJson(s: string, pos: number): unknown {
  let endPos: number | undefined
  parseJson.message = undefined
  let matches: RegExpExecArray | null
  if (pos) s = s.slice(pos)
  try {
    parseJson.position = pos + s.length
    return JSON.parse(s)
  } catch (e) {
    matches = rxParseJson.exec((e as Error).message)
    if (!matches) {
      parseJson.message = "unexpected end"
      return undefined
    }
    endPos = +matches[1]
    const c = s[endPos]
    s = s.slice(0, endPos)
    parseJson.position = pos + endPos
    try {
      return JSON.parse(s)
    } catch (e1) {
      parseJson.message = `unexpected token ${c}`
      return undefined
    }
  }
}

parseJson.message = undefined as string | undefined
parseJson.position = 0 as number
parseJson.code = 'require("ajv/dist/runtime/parseJson").parseJson'

export function parseJsonNumber(s: string, pos: number, maxDigits?: number): number | undefined {
  let numStr = ""
  let c: string
  parseJsonNumber.message = undefined
  if (s[pos] === "-") {
    numStr += "-"
    pos++
  }
  if (s[pos] === "0") {
    numStr += "0"
    pos++
  } else {
    if (!parseDigits(maxDigits)) {
      errorMessage()
      return undefined
    }
  }
  if (maxDigits) {
    parseJsonNumber.position = pos
    return +numStr
  }
  if (s[pos] === ".") {
    numStr += "."
    pos++
    if (!parseDigits()) {
      errorMessage()
      return undefined
    }
  }
  if (((c = s[pos]), c === "e" || c === "E")) {
    numStr += "e"
    pos++
    if (((c = s[pos]), c === "+" || c === "-")) {
      numStr += c
      pos++
    }
    if (!parseDigits()) {
      errorMessage()
      return undefined
    }
  }
  parseJsonNumber.position = pos
  return +numStr

  function parseDigits(maxLen?: number): boolean {
    let digit = false
    while (((c = s[pos]), c >= "0" && c <= "9" && (maxLen === undefined || maxLen-- > 0))) {
      digit = true
      numStr += c
      pos++
    }
    return digit
  }

  function errorMessage(): void {
    parseJsonNumber.position = pos
    parseJsonNumber.message = pos < s.length ? `unexpected token ${s[pos]}` : "unexpected end"
  }
}

parseJsonNumber.message = undefined as string | undefined
parseJsonNumber.position = 0 as number
parseJsonNumber.code = 'require("ajv/dist/runtime/parseJson").parseJsonNumber'

const escapedChars: {[X in string]?: string} = {
  b: "\b",
  f: "\f",
  n: "\n",
  r: "\r",
  t: "\t",
  '"': '"',
  "/": "/",
  "\\": "\\",
}

const CODE_A: number = "a".charCodeAt(0)
const CODE_0: number = "0".charCodeAt(0)

export function parseJsonString(s: string, pos: number): string | undefined {
  let str = ""
  let c: string | undefined
  parseJsonString.message = undefined
  // eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition
  while (true) {
    c = s[pos++]
    if (c === '"') break
    if (c === "\\") {
      c = s[pos]
      if (c in escapedChars) {
        str += escapedChars[c]
        pos++
      } else if (c === "u") {
        pos++
        let count = 4
        let code = 0
        while (count--) {
          code <<= 4
          c = s[pos]
          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
          if (c === undefined) {
            errorMessage("unexpected end")
            return undefined
          }
          c = c.toLowerCase()
          if (c >= "a" && c <= "f") {
            code += c.charCodeAt(0) - CODE_A + 10
          } else if (c >= "0" && c <= "9") {
            code += c.charCodeAt(0) - CODE_0
          } else {
            errorMessage(`unexpected token ${c}`)
            return undefined
          }
          pos++
        }
        str += String.fromCharCode(code)
      } else {
        errorMessage(`unexpected token ${c}`)
        return undefined
      }
      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    } else if (c === undefined) {
      errorMessage("unexpected end")
      return undefined
    } else {
      if (c.charCodeAt(0) >= 0x20) {
        str += c
      } else {
        errorMessage(`unexpected token ${c}`)
        return undefined
      }
    }
  }
  parseJsonString.position = pos
  return str

  function errorMessage(msg: string): void {
    parseJsonString.position = pos
    parseJsonString.message = msg
  }
}

parseJsonString.message = undefined as string | undefined
parseJsonString.position = 0 as number
parseJsonString.code = 'require("ajv/dist/runtime/parseJson").parseJsonString'


code:
const rxEscapable =
  // eslint-disable-next-line no-control-regex, no-misleading-character-class
  /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g

const escaped: {[K in string]?: string} = {
  "\b": "\\b",
  "\t": "\\t",
  "\n": "\\n",
  "\f": "\\f",
  "\r": "\\r",
  '"': '\\"',
  "\\": "\\\\",
}

export default function quote(s: string): string {
  rxEscapable.lastIndex = 0
  return (
    '"' +
    (rxEscapable.test(s)
      ? s.replace(rxEscapable, (a) => {
          const c = escaped[a]
          return typeof c === "string"
            ? c
            : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
        })
      : s) +
    '"'
  )
}

quote.code = 'require("ajv/dist/runtime/quote").default'


code:
import * as re2 from "re2"

type Re2 = typeof re2 & {code: string}
;(re2 as Re2).code = 'require("ajv/dist/runtime/re2").default'

export default re2 as Re2


code:
const DT_SEPARATOR = /t|\s/i
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
const TIME = /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:z|([+-]\d\d)(?::?(\d\d))?)$/i
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

export default function validTimestamp(str: string, allowDate: boolean): boolean {
  // http://tools.ietf.org/html/rfc3339#section-5.6
  const dt: string[] = str.split(DT_SEPARATOR)
  return (
    (dt.length === 2 && validDate(dt[0]) && validTime(dt[1])) ||
    (allowDate && dt.length === 1 && validDate(dt[0]))
  )
}

function validDate(str: string): boolean {
  const matches: string[] | null = DATE.exec(str)
  if (!matches) return false
  const y: number = +matches[1]
  const m: number = +matches[2]
  const d: number = +matches[3]
  return (
    m >= 1 &&
    m <= 12 &&
    d >= 1 &&
    (d <= DAYS[m] ||
      // leap year: https://tools.ietf.org/html/rfc3339#appendix-C
      (m === 2 && d === 29 && (y % 100 === 0 ? y % 400 === 0 : y % 4 === 0)))
  )
}

function validTime(str: string): boolean {
  const matches: string[] | null = TIME.exec(str)
  if (!matches) return false
  const hr: number = +matches[1]
  const min: number = +matches[2]
  const sec: number = +matches[3]
  const tzH: number = +(matches[4] || 0)
  const tzM: number = +(matches[5] || 0)
  return (
    (hr <= 23 && min <= 59 && sec <= 59) ||
    // leap second
    (hr - tzH === 23 && min - tzM === 59 && sec === 60)
  )
}

validTimestamp.code = 'require("ajv/dist/runtime/timestamp").default'


code:
// https://mathiasbynens.be/notes/javascript-encoding
// https://github.com/bestiejs/punycode.js - punycode.ucs2.decode
export default function ucs2length(str: string): number {
  const len = str.length
  let length = 0
  let pos = 0
  let value: number
  while (pos < len) {
    length++
    value = str.charCodeAt(pos++)
    if (value >= 0xd800 && value <= 0xdbff && pos < len) {
      // high surrogate, and there is a next character
      value = str.charCodeAt(pos)
      if ((value & 0xfc00) === 0xdc00) pos++ // low surrogate
    }
  }
  return length
}

ucs2length.code = 'require("ajv/dist/runtime/ucs2length").default'


code:
import * as uri from "uri-js"

type URI = typeof uri & {code: string}
;(uri as URI).code = 'require("ajv/dist/runtime/uri").default'

export default uri as URI


code:
import type {ErrorObject} from "../types"

export default class ValidationError extends Error {
  readonly errors: Partial<ErrorObject>[]
  readonly ajv: true
  readonly validation: true

  constructor(errors: Partial<ErrorObject>[]) {
    super("validation failed")
    this.errors = errors
    this.ajv = this.validation = true
  }
}


code:
import type AjvCore from "../core"
import type {AnyValidateFunction, SourceCode} from "../types"
import type {SchemaEnv} from "../compile"
import {UsedScopeValues, UsedValueState, ValueScopeName, varKinds} from "../compile/codegen/scope"
import {_, nil, _Code, Code, getProperty, getEsmExportName} from "../compile/codegen/code"

function standaloneCode(
  ajv: AjvCore,
  refsOrFunc?: {[K in string]?: string} | AnyValidateFunction
): string {
  if (!ajv.opts.code.source) {
    throw new Error("moduleCode: ajv instance must have code.source option")
  }
  const {_n} = ajv.scope.opts
  return typeof refsOrFunc == "function"
    ? funcExportCode(refsOrFunc.source)
    : refsOrFunc !== undefined
    ? multiExportsCode<string>(refsOrFunc, getValidate)
    : multiExportsCode<SchemaEnv>(ajv.schemas, (sch) =>
        sch.meta ? undefined : ajv.compile(sch.schema)
      )

  function getValidate(id: string): AnyValidateFunction {
    const v = ajv.getSchema(id)
    if (!v) throw new Error(`moduleCode: no schema with id ${id}`)
    return v
  }

  function funcExportCode(source?: SourceCode): string {
    const usedValues: UsedScopeValues = {}
    const n = source?.validateName
    const vCode = validateCode(usedValues, source)
    if (ajv.opts.code.esm) {
      // Always do named export as `validate` rather than the variable `n` which is `validateXX` for known export value
      return `"use strict";${_n}export const validate = ${n};${_n}export default ${n};${_n}${vCode}`
    }
    return `"use strict";${_n}module.exports = ${n};${_n}module.exports.default = ${n};${_n}${vCode}`
  }

  function multiExportsCode<T extends SchemaEnv | string>(
    schemas: {[K in string]?: T},
    getValidateFunc: (schOrId: T) => AnyValidateFunction | undefined
  ): string {
    const usedValues: UsedScopeValues = {}
    let code = _`"use strict";`
    for (const name in schemas) {
      const v = getValidateFunc(schemas[name] as T)
      if (v) {
        const vCode = validateCode(usedValues, v.source)
        const exportSyntax = ajv.opts.code.esm
          ? _`export const ${getEsmExportName(name)}`
          : _`exports${getProperty(name)}`
        code = _`${code}${_n}${exportSyntax} = ${v.source?.validateName};${_n}${vCode}`
      }
    }
    return `${code}`
  }

  function validateCode(usedValues: UsedScopeValues, s?: SourceCode): Code {
    if (!s) throw new Error('moduleCode: function does not have "source" property')
    if (usedState(s.validateName) === UsedValueState.Completed) return nil
    setUsedState(s.validateName, UsedValueState.Started)

    const scopeCode = ajv.scope.scopeCode(s.scopeValues, usedValues, refValidateCode)
    const code = new _Code(`${scopeCode}${_n}${s.validateCode}`)
    return s.evaluated ? _`${code}${s.validateName}.evaluated = ${s.evaluated};${_n}` : code

    function refValidateCode(n: ValueScopeName): Code | undefined {
      const vRef = n.value?.ref
      if (n.prefix === "validate" && typeof vRef == "function") {
        const v = vRef as AnyValidateFunction
        return validateCode(usedValues, v.source)
      } else if ((n.prefix === "root" || n.prefix === "wrapper") && typeof vRef == "object") {
        const {validate, validateName} = vRef as SchemaEnv
        if (!validateName) throw new Error("ajv internal error")
        const def = ajv.opts.code.es5 ? varKinds.var : varKinds.const
        const wrapper = _`${def} ${n} = {validate: ${validateName}};`
        if (usedState(validateName) === UsedValueState.Started) return wrapper
        const vCode = validateCode(usedValues, validate?.source)
        return _`${wrapper}${_n}${vCode}`
      }
      return undefined
    }

    function usedState(name: ValueScopeName): UsedValueState | undefined {
      return usedValues[name.prefix]?.get(name)
    }

    function setUsedState(name: ValueScopeName, state: UsedValueState): void {
      const {prefix} = name
      const names = (usedValues[prefix] = usedValues[prefix] || new Map())
      names.set(name, state)
    }
  }
}

module.exports = exports = standaloneCode
Object.defineProperty(exports, "__esModule", {value: true})

export default standaloneCode


code:
import Ajv, {AnySchema, AnyValidateFunction, ErrorObject} from "../core"
import standaloneCode from "."
import * as requireFromString from "require-from-string"

export default class AjvPack {
  errors?: ErrorObject[] | null // errors from the last validation
  constructor(readonly ajv: Ajv) {}

  validate(schemaKeyRef: AnySchema | string, data: unknown): boolean | Promise<unknown> {
    return Ajv.prototype.validate.call(this, schemaKeyRef, data)
  }

  compile<T = unknown>(schema: AnySchema, meta?: boolean): AnyValidateFunction<T> {
    return this.getStandalone(this.ajv.compile<T>(schema, meta))
  }

  getSchema<T = unknown>(keyRef: string): AnyValidateFunction<T> | undefined {
    const v = this.ajv.getSchema<T>(keyRef)
    if (!v) return undefined
    return this.getStandalone(v)
  }

  private getStandalone<T = unknown>(v: AnyValidateFunction<T>): AnyValidateFunction<T> {
    return requireFromString(standaloneCode(this.ajv, v)) as AnyValidateFunction<T>
  }

  addSchema(...args: Parameters<typeof Ajv.prototype.addSchema>): AjvPack {
    this.ajv.addSchema.call(this.ajv, ...args)
    return this
  }

  addKeyword(...args: Parameters<typeof Ajv.prototype.addKeyword>): AjvPack {
    this.ajv.addKeyword.call(this.ajv, ...args)
    return this
  }
}


code:
import * as URI from "uri-js"
import type {CodeGen, Code, Name, ScopeValueSets, ValueScopeName} from "../compile/codegen"
import type {SchemaEnv, SchemaCxt, SchemaObjCxt} from "../compile"
import type {JSONType} from "../compile/rules"
import type {KeywordCxt} from "../compile/validate"
import type Ajv from "../core"

interface _SchemaObject {
  id?: string
  $id?: string
  $schema?: string
  [x: string]: any // TODO
}

export interface SchemaObject extends _SchemaObject {
  id?: string
  $id?: string
  $schema?: string
  $async?: false
  [x: string]: any // TODO
}

export interface AsyncSchema extends _SchemaObject {
  $async: true
}

export type AnySchemaObject = SchemaObject | AsyncSchema

export type Schema = SchemaObject | boolean

export type AnySchema = Schema | AsyncSchema

export type SchemaMap = {[Key in string]?: AnySchema}

export interface SourceCode {
  validateName: ValueScopeName
  validateCode: string
  scopeValues: ScopeValueSets
  evaluated?: Code
}

export interface DataValidationCxt<T extends string | number = string | number> {
  instancePath: string
  parentData: {[K in T]: any} // object or array
  parentDataProperty: T // string or number
  rootData: Record<string, any> | any[]
  dynamicAnchors: {[Ref in string]?: ValidateFunction}
}

export interface ValidateFunction<T = unknown> {
  // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
  (this: Ajv | any, data: any, dataCxt?: DataValidationCxt): data is T
  errors?: null | ErrorObject[]
  evaluated?: Evaluated
  schema: AnySchema
  schemaEnv: SchemaEnv
  source?: SourceCode
}

export interface JTDParser<T = unknown> {
  (json: string): T | undefined
  message?: string
  position?: number
}

export type EvaluatedProperties = {[K in string]?: true} | true

export type EvaluatedItems = number | true

export interface Evaluated {
  // determined at compile time if staticProps/Items is true
  props?: EvaluatedProperties
  items?: EvaluatedItems
  // whether props/items determined at compile time
  dynamicProps: boolean
  dynamicItems: boolean
}

export interface AsyncValidateFunction<T = unknown> extends ValidateFunction<T> {
  (...args: Parameters<ValidateFunction<T>>): Promise<T>
  $async: true
}

export type AnyValidateFunction<T = any> = ValidateFunction<T> | AsyncValidateFunction<T>

export interface ErrorObject<K extends string = string, P = Record<string, any>, S = unknown> {
  keyword: K
  instancePath: string
  schemaPath: string
  params: P
  // Added to validation errors of "propertyNames" keyword schema
  propertyName?: string
  // Excluded if option `messages` set to false.
  message?: string
  // These are added with the `verbose` option.
  schema?: S
  parentSchema?: AnySchemaObject
  data?: unknown
}

export type ErrorNoParams<K extends string, S = unknown> = ErrorObject<K, Record<string, never>, S>

interface _KeywordDef {
  keyword: string | string[]
  type?: JSONType | JSONType[] // data types that keyword applies to
  schemaType?: JSONType | JSONType[] // allowed type(s) of keyword value in the schema
  allowUndefined?: boolean // used for keywords that can be invoked by other keywords, not being present in the schema
  $data?: boolean // keyword supports [$data reference](../../docs/guide/combining-schemas.md#data-reference)
  implements?: string[] // other schema keywords that this keyword implements
  before?: string // keyword should be executed before this keyword (should be applicable to the same type)
  post?: boolean // keyword should be executed after other keywords without post flag
  metaSchema?: AnySchemaObject // meta-schema for keyword schema value - it is better to use schemaType where applicable
  validateSchema?: AnyValidateFunction // compiled keyword metaSchema - should not be passed
  dependencies?: string[] // keywords that must be present in the same schema
  error?: KeywordErrorDefinition
  $dataError?: KeywordErrorDefinition
}

export interface CodeKeywordDefinition extends _KeywordDef {
  code: (cxt: KeywordCxt, ruleType?: string) => void
  trackErrors?: boolean
}

export type MacroKeywordFunc = (
  schema: any,
  parentSchema: AnySchemaObject,
  it: SchemaCxt
) => AnySchema

export type CompileKeywordFunc = (
  schema: any,
  parentSchema: AnySchemaObject,
  it: SchemaObjCxt
) => DataValidateFunction

export interface DataValidateFunction {
  (...args: Parameters<ValidateFunction>): boolean | Promise<any>
  errors?: Partial<ErrorObject>[]
}

export interface SchemaValidateFunction {
  (
    schema: any,
    data: any,
    parentSchema?: AnySchemaObject,
    dataCxt?: DataValidationCxt
  ): boolean | Promise<any>
  errors?: Partial<ErrorObject>[]
}

export interface FuncKeywordDefinition extends _KeywordDef {
  validate?: SchemaValidateFunction | DataValidateFunction
  compile?: CompileKeywordFunc
  // schema: false makes validate not to expect schema (DataValidateFunction)
  schema?: boolean // requires "validate"
  modifying?: boolean
  async?: boolean
  valid?: boolean
  errors?: boolean | "full"
}

export interface MacroKeywordDefinition extends FuncKeywordDefinition {
  macro: MacroKeywordFunc
}

export type KeywordDefinition =
  | CodeKeywordDefinition
  | FuncKeywordDefinition
  | MacroKeywordDefinition

export type AddedKeywordDefinition = KeywordDefinition & {
  type: JSONType[]
  schemaType: JSONType[]
}

export interface KeywordErrorDefinition {
  message: string | Code | ((cxt: KeywordErrorCxt) => string | Code)
  params?: Code | ((cxt: KeywordErrorCxt) => Code)
}

export type Vocabulary = (KeywordDefinition | string)[]

export interface KeywordErrorCxt {
  gen: CodeGen
  keyword: string
  data: Name
  $data?: string | false
  schema: any // TODO
  parentSchema?: AnySchemaObject
  schemaCode: Code | number | boolean
  schemaValue: Code | number | boolean
  schemaType?: JSONType[]
  errsCount?: Name
  params: KeywordCxtParams
  it: SchemaCxt
}

export type KeywordCxtParams = {[P in string]?: Code | string | number}

export type FormatValidator<T extends string | number> = (data: T) => boolean

export type FormatCompare<T extends string | number> = (data1: T, data2: T) => number | undefined

export type AsyncFormatValidator<T extends string | number> = (data: T) => Promise<boolean>

export interface FormatDefinition<T extends string | number> {
  type?: T extends string ? "string" | undefined : "number"
  validate: FormatValidator<T> | (T extends string ? string | RegExp : never)
  async?: false | undefined
  compare?: FormatCompare<T>
}

export interface AsyncFormatDefinition<T extends string | number> {
  type?: T extends string ? "string" | undefined : "number"
  validate: AsyncFormatValidator<T>
  async: true
  compare?: FormatCompare<T>
}

export type AddedFormat =
  | true
  | RegExp
  | FormatValidator<string>
  | FormatDefinition<string>
  | FormatDefinition<number>
  | AsyncFormatDefinition<string>
  | AsyncFormatDefinition<number>

export type Format = AddedFormat | string

export interface RegExpEngine {
  (pattern: string, u: string): RegExpLike
  code: string
}

export interface RegExpLike {
  test: (s: string) => boolean
}

export interface UriResolver {
  parse(uri: string): URI.URIComponents
  resolve(base: string, path: string): string
  serialize(component: URI.URIComponents): string
}


code:
/* eslint-disable @typescript-eslint/no-empty-interface */
type StrictNullChecksWrapper<Name extends string, Type> = undefined extends null
  ? `strictNullChecks must be true in tsconfig to use ${Name}`
  : Type

type UnionToIntersection<U> = (U extends any ? (_: U) => void : never) extends (_: infer I) => void
  ? I
  : never

export type SomeJSONSchema = UncheckedJSONSchemaType<Known, true>

type UncheckedPartialSchema<T> = Partial<UncheckedJSONSchemaType<T, true>>

export type PartialSchema<T> = StrictNullChecksWrapper<"PartialSchema", UncheckedPartialSchema<T>>

type JSONType<T extends string, IsPartial extends boolean> = IsPartial extends true
  ? T | undefined
  : T

interface NumberKeywords {
  minimum?: number
  maximum?: number
  exclusiveMinimum?: number
  exclusiveMaximum?: number
  multipleOf?: number
  format?: string
}

interface StringKeywords {
  minLength?: number
  maxLength?: number
  pattern?: string
  format?: string
}

type UncheckedJSONSchemaType<T, IsPartial extends boolean> = (
  | // these two unions allow arbitrary unions of types
  {
      anyOf: readonly UncheckedJSONSchemaType<T, IsPartial>[]
    }
  | {
      oneOf: readonly UncheckedJSONSchemaType<T, IsPartial>[]
    }
  // this union allows for { type: (primitive)[] } style schemas
  | ({
      type: readonly (T extends number
        ? JSONType<"number" | "integer", IsPartial>
        : T extends string
        ? JSONType<"string", IsPartial>
        : T extends boolean
        ? JSONType<"boolean", IsPartial>
        : never)[]
    } & UnionToIntersection<
      T extends number
        ? NumberKeywords
        : T extends string
        ? StringKeywords
        : T extends boolean
        ? // eslint-disable-next-line @typescript-eslint/ban-types
          {}
        : never
    >)
  // this covers "normal" types; it's last so typescript looks to it first for errors
  | ((T extends number
      ? {
          type: JSONType<"number" | "integer", IsPartial>
        } & NumberKeywords
      : T extends string
      ? {
          type: JSONType<"string", IsPartial>
        } & StringKeywords
      : T extends boolean
      ? {
          type: JSONType<"boolean", IsPartial>
        }
      : T extends readonly [any, ...any[]]
      ? {
          // JSON AnySchema for tuple
          type: JSONType<"array", IsPartial>
          items: {
            readonly [K in keyof T]-?: UncheckedJSONSchemaType<T[K], false> & Nullable<T[K]>
          } & {length: T["length"]}
          minItems: T["length"]
        } & ({maxItems: T["length"]} | {additionalItems: false})
      : T extends readonly any[]
      ? {
          type: JSONType<"array", IsPartial>
          items: UncheckedJSONSchemaType<T[0], false>
          contains?: UncheckedPartialSchema<T[0]>
          minItems?: number
          maxItems?: number
          minContains?: number
          maxContains?: number
          uniqueItems?: true
          additionalItems?: never
        }
      : T extends Record<string, any>
      ? {
          // JSON AnySchema for records and dictionaries
          // "required" is not optional because it is often forgotten
          // "properties" are optional for more concise dictionary schemas
          // "patternProperties" and can be only used with interfaces that have string index
          type: JSONType<"object", IsPartial>
          additionalProperties?: boolean | UncheckedJSONSchemaType<T[string], false>
          unevaluatedProperties?: boolean | UncheckedJSONSchemaType<T[string], false>
          properties?: IsPartial extends true
            ? Partial<UncheckedPropertiesSchema<T>>
            : UncheckedPropertiesSchema<T>
          patternProperties?: Record<string, UncheckedJSONSchemaType<T[string], false>>
          propertyNames?: Omit<UncheckedJSONSchemaType<string, false>, "type"> & {type?: "string"}
          dependencies?: {[K in keyof T]?: Readonly<(keyof T)[]> | UncheckedPartialSchema<T>}
          dependentRequired?: {[K in keyof T]?: Readonly<(keyof T)[]>}
          dependentSchemas?: {[K in keyof T]?: UncheckedPartialSchema<T>}
          minProperties?: number
          maxProperties?: number
        } & (IsPartial extends true // "required" is not necessary if it's a non-partial type with no required keys // are listed it only asserts that optional cannot be listed. // "required" type does not guarantee that all required properties
          ? {required: Readonly<(keyof T)[]>}
          : [UncheckedRequiredMembers<T>] extends [never]
          ? {required?: Readonly<UncheckedRequiredMembers<T>[]>}
          : {required: Readonly<UncheckedRequiredMembers<T>[]>})
      : T extends null
      ? {
          type: JSONType<"null", IsPartial>
          nullable: true
        }
      : never) & {
      allOf?: Readonly<UncheckedPartialSchema<T>[]>
      anyOf?: Readonly<UncheckedPartialSchema<T>[]>
      oneOf?: Readonly<UncheckedPartialSchema<T>[]>
      if?: UncheckedPartialSchema<T>
      then?: UncheckedPartialSchema<T>
      else?: UncheckedPartialSchema<T>
      not?: UncheckedPartialSchema<T>
    })
) & {
  [keyword: string]: any
  $id?: string
  $ref?: string
  $defs?: Record<string, UncheckedJSONSchemaType<Known, true>>
  definitions?: Record<string, UncheckedJSONSchemaType<Known, true>>
}

export type JSONSchemaType<T> = StrictNullChecksWrapper<
  "JSONSchemaType",
  UncheckedJSONSchemaType<T, false>
>

type Known =
  | {[key: string]: Known}
  | [Known, ...Known[]]
  | Known[]
  | number
  | string
  | boolean
  | null

type UncheckedPropertiesSchema<T> = {
  [K in keyof T]-?: (UncheckedJSONSchemaType<T[K], false> & Nullable<T[K]>) | {$ref: string}
}

export type PropertiesSchema<T> = StrictNullChecksWrapper<
  "PropertiesSchema",
  UncheckedPropertiesSchema<T>
>

type UncheckedRequiredMembers<T> = {
  [K in keyof T]-?: undefined extends T[K] ? never : K
}[keyof T]

export type RequiredMembers<T> = StrictNullChecksWrapper<
  "RequiredMembers",
  UncheckedRequiredMembers<T>
>

type Nullable<T> = undefined extends T
  ? {
      nullable: true
      const?: null // any non-null value would fail `const: null`, `null` would fail any other value in const
      enum?: Readonly<(T | null)[]> // `null` must be explicitly included in "enum" for `null` to pass
      default?: T | null
    }
  : {
      nullable?: false
      const?: T
      enum?: Readonly<T[]>
      default?: T
    }


code:
/** numeric strings */
type NumberType = "float32" | "float64" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32"

/** string strings */
type StringType = "string" | "timestamp"

/** Generic JTD Schema without inference of the represented type */
export type SomeJTDSchemaType = (
  | // ref
  {ref: string}
  // primitives
  | {type: NumberType | StringType | "boolean"}
  // enum
  | {enum: string[]}
  // elements
  | {elements: SomeJTDSchemaType}
  // values
  | {values: SomeJTDSchemaType}
  // properties
  | {
      properties: Record<string, SomeJTDSchemaType>
      optionalProperties?: Record<string, SomeJTDSchemaType>
      additionalProperties?: boolean
    }
  | {
      properties?: Record<string, SomeJTDSchemaType>
      optionalProperties: Record<string, SomeJTDSchemaType>
      additionalProperties?: boolean
    }
  // discriminator
  | {discriminator: string; mapping: Record<string, SomeJTDSchemaType>}
  // empty
  // NOTE see the end of
  // https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-675156492
  // eslint-disable-next-line @typescript-eslint/ban-types
  | {}
) & {
  nullable?: boolean
  metadata?: Record<string, unknown>
  definitions?: Record<string, SomeJTDSchemaType>
}

/** required keys of an object, not undefined */
type RequiredKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? never : K
}[keyof T]

/** optional or undifined-able keys of an object */
type OptionalKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? K : never
}[keyof T]

/** type is true if T is a union type */
type IsUnion_<T, U extends T = T> = false extends (
  T extends unknown ? ([U] extends [T] ? false : true) : never
)
  ? false
  : true
type IsUnion<T> = IsUnion_<T>

/** type is true if T is identically E */
type TypeEquality<T, E> = [T] extends [E] ? ([E] extends [T] ? true : false) : false

/** type is true if T or null is identically E or null*/
type NullTypeEquality<T, E> = TypeEquality<T | null, E | null>

/** gets only the string literals of a type or null if a type isn't a string literal */
type EnumString<T> = [T] extends [never]
  ? null
  : T extends string
  ? string extends T
    ? null
    : T
  : null

/** true if type is a union of string literals */
type IsEnum<T> = null extends EnumString<T> ? false : true

/** true only if all types are array types (not tuples) */
// NOTE relies on the fact that tuples don't have an index at 0.5, but arrays
// have an index at every number
type IsElements<T> = false extends IsUnion<T>
  ? [T] extends [readonly unknown[]]
    ? undefined extends T[0.5]
      ? false
      : true
    : false
  : false

/** true if the the type is a values type */
type IsValues<T> = false extends IsUnion<T> ? TypeEquality<keyof T, string> : false

/** true if type is a properties type and Union is false, or type is a discriminator type and Union is true */
type IsRecord<T, Union extends boolean> = Union extends IsUnion<T>
  ? null extends EnumString<keyof T>
    ? false
    : true
  : false

/** true if type represents an empty record */
type IsEmptyRecord<T> = [T] extends [Record<string, never>]
  ? [T] extends [never]
    ? false
    : true
  : false

/** actual schema */
export type JTDSchemaType<T, D extends Record<string, unknown> = Record<string, never>> = (
  | // refs - where null wasn't specified, must match exactly
  (null extends EnumString<keyof D>
      ? never
      :
          | ({[K in keyof D]: [T] extends [D[K]] ? {ref: K} : never}[keyof D] & {nullable?: false})
          // nulled refs - if ref is nullable and nullable is specified, then it can
          // match either null or non-null definitions
          | (null extends T
              ? {
                  [K in keyof D]: [Exclude<T, null>] extends [Exclude<D[K], null>]
                    ? {ref: K}
                    : never
                }[keyof D] & {nullable: true}
              : never))
  // empty - empty schemas also treat nullable differently in that it's now fully ignored
  | (unknown extends T ? {nullable?: boolean} : never)
  // all other types // numbers - only accepts the type number
  | ((true extends NullTypeEquality<T, number>
      ? {type: NumberType}
      : // booleans - accepts the type boolean
      true extends NullTypeEquality<T, boolean>
      ? {type: "boolean"}
      : // strings - only accepts the type string
      true extends NullTypeEquality<T, string>
      ? {type: StringType}
      : // strings - only accepts the type Date
      true extends NullTypeEquality<T, Date>
      ? {type: "timestamp"}
      : // enums - only accepts union of string literals
      // TODO we can't actually check that everything in the union was specified
      true extends IsEnum<Exclude<T, null>>
      ? {enum: EnumString<Exclude<T, null>>[]}
      : // arrays - only accepts arrays, could be array of unions to be resolved later
      true extends IsElements<Exclude<T, null>>
      ? T extends readonly (infer E)[]
        ? {
            elements: JTDSchemaType<E, D>
          }
        : never
      : // empty properties
      true extends IsEmptyRecord<Exclude<T, null>>
      ?
          | {properties: Record<string, never>; optionalProperties?: Record<string, never>}
          | {optionalProperties: Record<string, never>}
      : // values
      true extends IsValues<Exclude<T, null>>
      ? T extends Record<string, infer V>
        ? {
            values: JTDSchemaType<V, D>
          }
        : never
      : // properties
      true extends IsRecord<Exclude<T, null>, false>
      ? ([RequiredKeys<Exclude<T, null>>] extends [never]
          ? {
              properties?: Record<string, never>
            }
          : {
              properties: {[K in RequiredKeys<T>]: JTDSchemaType<T[K], D>}
            }) &
          ([OptionalKeys<Exclude<T, null>>] extends [never]
            ? {
                optionalProperties?: Record<string, never>
              }
            : {
                optionalProperties: {
                  [K in OptionalKeys<T>]: JTDSchemaType<Exclude<T[K], undefined>, D>
                }
              }) & {
            additionalProperties?: boolean
          }
      : // discriminator
      true extends IsRecord<Exclude<T, null>, true>
      ? {
          [K in keyof Exclude<T, null>]-?: Exclude<T, null>[K] extends string
            ? {
                discriminator: K
                mapping: {
                  // TODO currently allows descriminator to be present in schema
                  [M in Exclude<T, null>[K]]: JTDSchemaType<
                    Omit<T extends Record<K, M> ? T : never, K>,
                    D
                  >
                }
              }
            : never
        }[keyof Exclude<T, null>]
      : never) &
      (null extends T
        ? {
            nullable: true
          }
        : {nullable?: false}))
) & {
  // extra properties
  metadata?: Record<string, unknown>
  // TODO these should only be allowed at the top level
  definitions?: {[K in keyof D]: JTDSchemaType<D[K], D>}
}

type JTDDataDef<S, D extends Record<string, unknown>> =
  | // ref
  (S extends {ref: string}
      ? D extends {[K in S["ref"]]: infer V}
        ? JTDDataDef<V, D>
        : never
      : // type
      S extends {type: NumberType}
      ? number
      : S extends {type: "boolean"}
      ? boolean
      : S extends {type: "string"}
      ? string
      : S extends {type: "timestamp"}
      ? string | Date
      : // enum
      S extends {enum: readonly (infer E)[]}
      ? string extends E
        ? never
        : [E] extends [string]
        ? E
        : never
      : // elements
      S extends {elements: infer E}
      ? JTDDataDef<E, D>[]
      : // properties
      S extends {
          properties: Record<string, unknown>
          optionalProperties?: Record<string, unknown>
          additionalProperties?: boolean
        }
      ? {-readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D>} & {
          -readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef<
            S["optionalProperties"][K],
            D
          >
        } & ([S["additionalProperties"]] extends [true] ? Record<string, unknown> : unknown)
      : S extends {
          properties?: Record<string, unknown>
          optionalProperties: Record<string, unknown>
          additionalProperties?: boolean
        }
      ? {-readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D>} & {
          -readonly [K in keyof S["optionalProperties"]]+?: JTDDataDef<
            S["optionalProperties"][K],
            D
          >
        } & ([S["additionalProperties"]] extends [true] ? Record<string, unknown> : unknown)
      : // values
      S extends {values: infer V}
      ? Record<string, JTDDataDef<V, D>>
      : // discriminator
      S extends {discriminator: infer M; mapping: Record<string, unknown>}
      ? [M] extends [string]
        ? {
            [K in keyof S["mapping"]]: JTDDataDef<S["mapping"][K], D> & {[KM in M]: K}
          }[keyof S["mapping"]]
        : never
      : // empty
        unknown)
  | (S extends {nullable: true} ? null : never)

export type JTDDataType<S> = S extends {definitions: Record<string, unknown>}
  ? JTDDataDef<S, S["definitions"]>
  : JTDDataDef<S, Record<string, never>>


code:
import type {
  CodeKeywordDefinition,
  ErrorObject,
  KeywordErrorDefinition,
  AnySchema,
} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, str, not, Name} from "../../compile/codegen"
import {alwaysValidSchema, checkStrictMode, Type} from "../../compile/util"

export type AdditionalItemsError = ErrorObject<"additionalItems", {limit: number}, AnySchema>

const error: KeywordErrorDefinition = {
  message: ({params: {len}}) => str`must NOT have more than ${len} items`,
  params: ({params: {len}}) => _`{limit: ${len}}`,
}

const def: CodeKeywordDefinition = {
  keyword: "additionalItems" as const,
  type: "array",
  schemaType: ["boolean", "object"],
  before: "uniqueItems",
  error,
  code(cxt: KeywordCxt) {
    const {parentSchema, it} = cxt
    const {items} = parentSchema
    if (!Array.isArray(items)) {
      checkStrictMode(it, '"additionalItems" is ignored when "items" is not an array of schemas')
      return
    }
    validateAdditionalItems(cxt, items)
  },
}

export function validateAdditionalItems(cxt: KeywordCxt, items: AnySchema[]): void {
  const {gen, schema, data, keyword, it} = cxt
  it.items = true
  const len = gen.const("len", _`${data}.length`)
  if (schema === false) {
    cxt.setParams({len: items.length})
    cxt.pass(_`${len} <= ${items.length}`)
  } else if (typeof schema == "object" && !alwaysValidSchema(it, schema)) {
    const valid = gen.var("valid", _`${len} <= ${items.length}`) // TODO var
    gen.if(not(valid), () => validateItems(valid))
    cxt.ok(valid)
  }

  function validateItems(valid: Name): void {
    gen.forRange("i", items.length, len, (i) => {
      cxt.subschema({keyword, dataProp: i, dataPropType: Type.Num}, valid)
      if (!it.allErrors) gen.if(not(valid), () => gen.break())
    })
  }
}

export default def


code:
import type {
  CodeKeywordDefinition,
  AddedKeywordDefinition,
  ErrorObject,
  KeywordErrorDefinition,
  AnySchema,
} from "../../types"
import {allSchemaProperties, usePattern, isOwnProperty} from "../code"
import {_, nil, or, not, Code, Name} from "../../compile/codegen"
import N from "../../compile/names"
import type {SubschemaArgs} from "../../compile/validate/subschema"
import {alwaysValidSchema, schemaRefOrVal, Type} from "../../compile/util"

export type AdditionalPropertiesError = ErrorObject<
  "additionalProperties",
  {additionalProperty: string},
  AnySchema
>

const error: KeywordErrorDefinition = {
  message: "must NOT have additional properties",
  params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`,
}

const def: CodeKeywordDefinition & AddedKeywordDefinition = {
  keyword: "additionalProperties",
  type: ["object"],
  schemaType: ["boolean", "object"],
  allowUndefined: true,
  trackErrors: true,
  error,
  code(cxt) {
    const {gen, schema, parentSchema, data, errsCount, it} = cxt
    /* istanbul ignore if */
    if (!errsCount) throw new Error("ajv implementation error")
    const {allErrors, opts} = it
    it.props = true
    if (opts.removeAdditional !== "all" && alwaysValidSchema(it, schema)) return
    const props = allSchemaProperties(parentSchema.properties)
    const patProps = allSchemaProperties(parentSchema.patternProperties)
    checkAdditionalProperties()
    cxt.ok(_`${errsCount} === ${N.errors}`)

    function checkAdditionalProperties(): void {
      gen.forIn("key", data, (key: Name) => {
        if (!props.length && !patProps.length) additionalPropertyCode(key)
        else gen.if(isAdditional(key), () => additionalPropertyCode(key))
      })
    }

    function isAdditional(key: Name): Code {
      let definedProp: Code
      if (props.length > 8) {
        // TODO maybe an option instead of hard-coded 8?
        const propsSchema = schemaRefOrVal(it, parentSchema.properties, "properties")
        definedProp = isOwnProperty(gen, propsSchema as Code, key)
      } else if (props.length) {
        definedProp = or(...props.map((p) => _`${key} === ${p}`))
      } else {
        definedProp = nil
      }
      if (patProps.length) {
        definedProp = or(definedProp, ...patProps.map((p) => _`${usePattern(cxt, p)}.test(${key})`))
      }
      return not(definedProp)
    }

    function deleteAdditional(key: Name): void {
      gen.code(_`delete ${data}[${key}]`)
    }

    function additionalPropertyCode(key: Name): void {
      if (opts.removeAdditional === "all" || (opts.removeAdditional && schema === false)) {
        deleteAdditional(key)
        return
      }

      if (schema === false) {
        cxt.setParams({additionalProperty: key})
        cxt.error()
        if (!allErrors) gen.break()
        return
      }

      if (typeof schema == "object" && !alwaysValidSchema(it, schema)) {
        const valid = gen.name("valid")
        if (opts.removeAdditional === "failing") {
          applyAdditionalSchema(key, valid, false)
          gen.if(not(valid), () => {
            cxt.reset()
            deleteAdditional(key)
          })
        } else {
          applyAdditionalSchema(key, valid)
          if (!allErrors) gen.if(not(valid), () => gen.break())
        }
      }
    }

    function applyAdditionalSchema(key: Name, valid: Name, errors?: false): void {
      const subschema: SubschemaArgs = {
        keyword: "additionalProperties",
        dataProp: key,
        dataPropType: Type.Str,
      }
      if (errors === false) {
        Object.assign(subschema, {
          compositeRule: true,
          createErrors: false,
          allErrors: false,
        })
      }
      cxt.subschema(subschema, valid)
    }
  },
}

export default def


code:
import type {CodeKeywordDefinition, AnySchema} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {alwaysValidSchema} from "../../compile/util"

const def: CodeKeywordDefinition = {
  keyword: "allOf",
  schemaType: "array",
  code(cxt: KeywordCxt) {
    const {gen, schema, it} = cxt
    /* istanbul ignore if */
    if (!Array.isArray(schema)) throw new Error("ajv implementation error")
    const valid = gen.name("valid")
    schema.forEach((sch: AnySchema, i: number) => {
      if (alwaysValidSchema(it, sch)) return
      const schCxt = cxt.subschema({keyword: "allOf", schemaProp: i}, valid)
      cxt.ok(valid)
      cxt.mergeEvaluated(schCxt)
    })
  },
}

export default def


code:
import type {CodeKeywordDefinition, ErrorNoParams, AnySchema} from "../../types"
import {validateUnion} from "../code"

export type AnyOfError = ErrorNoParams<"anyOf", AnySchema[]>

const def: CodeKeywordDefinition = {
  keyword: "anyOf",
  schemaType: "array",
  trackErrors: true,
  code: validateUnion,
  error: {message: "must match a schema in anyOf"},
}

export default def


code:
import type {
  CodeKeywordDefinition,
  KeywordErrorDefinition,
  ErrorObject,
  AnySchema,
} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, str, Name} from "../../compile/codegen"
import {alwaysValidSchema, checkStrictMode, Type} from "../../compile/util"

export type ContainsError = ErrorObject<
  "contains",
  {minContains: number; maxContains?: number},
  AnySchema
>

const error: KeywordErrorDefinition = {
  message: ({params: {min, max}}) =>
    max === undefined
      ? str`must contain at least ${min} valid item(s)`
      : str`must contain at least ${min} and no more than ${max} valid item(s)`,
  params: ({params: {min, max}}) =>
    max === undefined ? _`{minContains: ${min}}` : _`{minContains: ${min}, maxContains: ${max}}`,
}

const def: CodeKeywordDefinition = {
  keyword: "contains",
  type: "array",
  schemaType: ["object", "boolean"],
  before: "uniqueItems",
  trackErrors: true,
  error,
  code(cxt: KeywordCxt) {
    const {gen, schema, parentSchema, data, it} = cxt
    let min: number
    let max: number | undefined
    const {minContains, maxContains} = parentSchema
    if (it.opts.next) {
      min = minContains === undefined ? 1 : minContains
      max = maxContains
    } else {
      min = 1
    }
    const len = gen.const("len", _`${data}.length`)
    cxt.setParams({min, max})
    if (max === undefined && min === 0) {
      checkStrictMode(it, `"minContains" == 0 without "maxContains": "contains" keyword ignored`)
      return
    }
    if (max !== undefined && min > max) {
      checkStrictMode(it, `"minContains" > "maxContains" is always invalid`)
      cxt.fail()
      return
    }
    if (alwaysValidSchema(it, schema)) {
      let cond = _`${len} >= ${min}`
      if (max !== undefined) cond = _`${cond} && ${len} <= ${max}`
      cxt.pass(cond)
      return
    }

    it.items = true
    const valid = gen.name("valid")
    if (max === undefined && min === 1) {
      validateItems(valid, () => gen.if(valid, () => gen.break()))
    } else if (min === 0) {
      gen.let(valid, true)
      if (max !== undefined) gen.if(_`${data}.length > 0`, validateItemsWithCount)
    } else {
      gen.let(valid, false)
      validateItemsWithCount()
    }
    cxt.result(valid, () => cxt.reset())

    function validateItemsWithCount(): void {
      const schValid = gen.name("_valid")
      const count = gen.let("count", 0)
      validateItems(schValid, () => gen.if(schValid, () => checkLimits(count)))
    }

    function validateItems(_valid: Name, block: () => void): void {
      gen.forRange("i", 0, len, (i) => {
        cxt.subschema(
          {
            keyword: "contains",
            dataProp: i,
            dataPropType: Type.Num,
            compositeRule: true,
          },
          _valid
        )
        block()
      })
    }

    function checkLimits(count: Name): void {
      gen.code(_`${count}++`)
      if (max === undefined) {
        gen.if(_`${count} >= ${min}`, () => gen.assign(valid, true).break())
      } else {
        gen.if(_`${count} > ${max}`, () => gen.assign(valid, false).break())
        if (min === 1) gen.assign(valid, true)
        else gen.if(_`${count} >= ${min}`, () => gen.assign(valid, true))
      }
    }
  },
}

export default def


code:
import type {
  CodeKeywordDefinition,
  ErrorObject,
  KeywordErrorDefinition,
  SchemaMap,
  AnySchema,
} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_, str} from "../../compile/codegen"
import {alwaysValidSchema} from "../../compile/util"
import {checkReportMissingProp, checkMissingProp, reportMissingProp, propertyInData} from "../code"

export type PropertyDependencies = {[K in string]?: string[]}

export interface DependenciesErrorParams {
  property: string
  missingProperty: string
  depsCount: number
  deps: string // TODO change to string[]
}

type SchemaDependencies = SchemaMap

export type DependenciesError = ErrorObject<
  "dependencies",
  DependenciesErrorParams,
  {[K in string]?: string[] | AnySchema}
>

export const error: KeywordErrorDefinition = {
  message: ({params: {property, depsCount, deps}}) => {
    const property_ies = depsCount === 1 ? "property" : "properties"
    return str`must have ${property_ies} ${deps} when property ${property} is present`
  },
  params: ({params: {property, depsCount, deps, missingProperty}}) =>
    _`{property: ${property},
    missingProperty: ${missingProperty},
    depsCount: ${depsCount},
    deps: ${deps}}`, // TODO change to reference
}

const def: CodeKeywordDefinition = {
  keyword: "dependencies",
  type: "object",
  schemaType: "object",
  error,
  code(cxt: KeywordCxt) {
    const [propDeps, schDeps] = splitDependencies(cxt)
    validatePropertyDeps(cxt, propDeps)
    validateSchemaDeps(cxt, schDeps)
  },
}

function splitDependencies({schema}: KeywordCxt): [PropertyDependencies, SchemaDependencies] {
  const propertyDeps: PropertyDependencies = {}
  const schemaDeps: SchemaDependencies = {}
  for (const key in schema) {
    if (key === "__proto__") continue
    const deps = Array.isArray(schema[key]) ? propertyDeps : schemaDeps
    deps[key] = schema[key]
  }
  return [propertyDeps, schemaDeps]
}

export function validatePropertyDeps(
  cxt: KeywordCxt,
  propertyDeps: {[K in string]?: string[]} = cxt.schema
): void {
  const {gen, data, it} = cxt
  if (Object.keys(propertyDeps).length === 0) return
  const missing = gen.let("missing")
  for (const prop in propertyDeps) {
    const deps = propertyDeps[prop] as string[]
    if (deps.length === 0) continue
    const hasProperty = propertyInData(gen, data, prop, it.opts.ownProperties)
    cxt.setParams({
      property: prop,
      depsCount: deps.length,
      deps: deps.join(", "),
    })
    if (it.allErrors) {
      gen.if(hasProperty, () => {
        for (const depProp of deps) {
          checkReportMissingProp(cxt, depProp)
        }
      })
    } else {
      gen.if(_`${hasProperty} && (${checkMissingProp(cxt, deps, missing)})`)
      reportMissingProp(cxt, missing)
      gen.else()
    }
  }
}

export function validateSchemaDeps(cxt: KeywordCxt, schemaDeps: SchemaMap = cxt.schema): void {
  const {gen, data, keyword, it} = cxt
  const valid = gen.name("valid")
  for (const prop in schemaDeps) {
    if (alwaysValidSchema(it, schemaDeps[prop] as AnySchema)) continue
    gen.if(
      propertyInData(gen, data, prop, it.opts.ownProperties),
      () => {
        const schCxt = cxt.subschema({keyword, schemaProp: prop}, valid)
        cxt.mergeValidEvaluated(schCxt, valid)
      },
      () => gen.var(valid, true) // TODO var
    )
    cxt.ok(valid)
  }
}

export default def


code:
import type {CodeKeywordDefinition} from "../../types"
import {validateSchemaDeps} from "./dependencies"

const def: CodeKeywordDefinition = {
  keyword: "dependentSchemas",
  type: "object",
  schemaType: "object",
  code: (cxt) => validateSchemaDeps(cxt),
}

export default def


code:
import type {
  CodeKeywordDefinition,
  ErrorObject,
  KeywordErrorDefinition,
  AnySchema,
} from "../../types"
import type {SchemaObjCxt} from "../../compile"
import type {KeywordCxt} from "../../compile/validate"
import {_, str, not, Name} from "../../compile/codegen"
import {alwaysValidSchema, checkStrictMode} from "../../compile/util"

export type IfKeywordError = ErrorObject<"if", {failingKeyword: string}, AnySchema>

const error: KeywordErrorDefinition = {
  message: ({params}) => str`must match "${params.ifClause}" schema`,
  params: ({params}) => _`{failingKeyword: ${params.ifClause}}`,
}

const def: CodeKeywordDefinition = {
  keyword: "if",
  schemaType: ["object", "boolean"],
  trackErrors: true,
  error,
  code(cxt: KeywordCxt) {
    const {gen, parentSchema, it} = cxt
    if (parentSchema.then === undefined && parentSchema.else === undefined) {
      checkStrictMode(it, '"if" without "then" and "else" is ignored')
    }
    const hasThen = hasSchema(it, "then")
    const hasElse = hasSchema(it, "else")
    if (!hasThen && !hasElse) return

    const valid = gen.let("valid", true)
    const schValid = gen.name("_valid")
    validateIf()
    cxt.reset()

    if (hasThen && hasElse) {
      const ifClause = gen.let("ifClause")
      cxt.setParams({ifClause})
      gen.if(schValid, validateClause("then", ifClause), validateClause("else", ifClause))
    } else if (hasThen) {
      gen.if(schValid, validateClause("then"))
    } else {
      gen.if(not(schValid), validateClause("else"))
    }

    cxt.pass(valid, () => cxt.error(true))

    function validateIf(): void {
      const schCxt = cxt.subschema(
        {
          keyword: "if",
          compositeRule: true,
          createErrors: false,
          allErrors: false,
        },
        schValid
      )
      cxt.mergeEvaluated(schCxt)
    }

    function validateClause(keyword: string, ifClause?: Name): () => void {
      return () => {
        const schCxt = cxt.subschema({keyword}, schValid)
        gen.assign(valid, schValid)
        cxt.mergeValidEvaluated(schCxt, valid)
        if (ifClause) gen.assign(ifClause, _`${keyword}`)
        else cxt.setParams({ifClause: keyword})
      }
    }
  },
}

function hasSchema(it: SchemaObjCxt, keyword: string): boolean {
  const schema = it.schema[keyword]
  return schema !== undefined && !alwaysValidSchema(it, schema)
}

export default def


code:
import type {ErrorNoParams, Vocabulary} from "../../types"
import additionalItems, {AdditionalItemsError} from "./additionalItems"
import prefixItems from "./prefixItems"
import items from "./items"
import items2020, {ItemsError} from "./items2020"
import contains, {ContainsError} from "./contains"
import dependencies, {DependenciesError} from "./dependencies"
import propertyNames, {PropertyNamesError} from "./propertyNames"
import additionalProperties, {AdditionalPropertiesError} from "./additionalProperties"
import properties from "./properties"
import patternProperties from "./patternProperties"
import notKeyword, {NotKeywordError} from "./not"
import anyOf, {AnyOfError} from "./anyOf"
import oneOf, {OneOfError} from "./oneOf"
import allOf from "./allOf"
import ifKeyword, {IfKeywordError} from "./if"
import thenElse from "./thenElse"

export default function getApplicator(draft2020 = false): Vocabulary {
  const applicator = [
    // any
    notKeyword,
    anyOf,
    oneOf,
    allOf,
    ifKeyword,
    thenElse,
    // object
    propertyNames,
    additionalProperties,
    dependencies,
    properties,
    patternProperties,
  ]
  // array
  if (draft2020) applicator.push(prefixItems, items2020)
  else applicator.push(additionalItems, items)
  applicator.push(contains)
  return applicator
}

export type ApplicatorKeywordError =
  | ErrorNoParams<"false schema">
  | AdditionalItemsError
  | ItemsError
  | ContainsError
  | AdditionalPropertiesError
  | DependenciesError
  | IfKeywordError
  | AnyOfError
  | OneOfError
  | NotKeywordError
  | PropertyNamesError


code:
import type {CodeKeywordDefinition, AnySchema, AnySchemaObject} from "../../types"
import type {KeywordCxt} from "../../compile/validate"
import {_} from "../../compile/codegen"
import {alwaysValidSchema, mergeEvaluated, checkStrictMode} from "../../compile/util"
import {validateArray} from "../code"

const def: CodeKeywordDefinition = {
  keyword: "items",
  type: "array",
  schemaType: ["object", "array", "boolean"],
  before: "uniqueItems",
  code(cxt: KeywordCxt) {
    const {schema, it} = cxt
    if (Array.isArray(schema)) return validateTuple(cxt, "additionalItems", schema)
    it.items = true
    if (alwaysValidSchema(it, schema)) return
    cxt.ok(validateArray(cxt))
  },
}

export function validateTuple(
  cxt: KeywordCxt,
  extraItems: string,
  schArr: AnySchema[] = cxt.schema
): void {
  const {gen, parentSchema, data, keyword, it} = cxt
  checkStrictTuple(parentSchema)
  if (it.opts.unevaluated && schArr.length && it.items !== true) {
    it.items = mergeEvaluated.items(gen, schArr.length, it.items)
  }
  const valid = gen.name("valid")
  const len = gen.const("len", _`${data}.length`)
  schArr.forEach((sch: AnySchema, i: number) => {
    if (alwaysValidSchema(it, sch)) return
    gen.if(_`${len} > ${i}`, () =>
      cxt.subschema(
        {
          keyword,
          schemaProp: i,
          dataProp: i,
        },
        valid
      )
    )
    cxt.ok(valid)
  })

  function checkStrictTuple(sch: AnySchemaObject): void {
    const {opts, errSchemaPath} = it
    const l = schArr.length
    const fullTuple = l === sch.minItems && (l === sch.maxItems || sch[extraItems] === false)
    if (opts.strictTuples && !fullTuple) {
      const msg = `"${keyword}" is ${l}-tuple, but minItems or maxItems/${extraItems} are not specified or different at path "${errSchemaPath}"`
      checkStrictMode(it, msg, opts.strictTuples)
    }
  }
}

export default def