epoberezkin/ajv

View on GitHub
lib/vocabularies/applicator/contains.ts

Summary

Maintainability
C
1 day
Test Coverage
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