import type Ajv from "ajv" import type { Plugin, CodeKeywordDefinition, KeywordErrorDefinition, Code, Name, ErrorObject, } from "ajv" import type {AddedFormat} from "ajv/dist/types" import type {Rule} from "ajv/dist/compile/rules" import KeywordCxt from "ajv/dist/compile/context" import {_, str, or, getProperty, operators} from "ajv/dist/compile/codegen" type Kwd = "formatMaximum" | "formatMinimum" | "formatExclusiveMaximum" | "formatExclusiveMinimum" type Comparison = "<=" | ">=" | "<" | ">" const ops = operators const KWDs: {[K in Kwd]: {okStr: Comparison; ok: Code; fail: Code}} = { formatMaximum: {okStr: "<=", ok: ops.LTE, fail: ops.GT}, formatMinimum: {okStr: ">=", ok: ops.GTE, fail: ops.LT}, formatExclusiveMaximum: {okStr: "<", ok: ops.LT, fail: ops.GTE}, formatExclusiveMinimum: {okStr: ">", ok: ops.GT, fail: ops.LTE}, } export type LimitFormatError = ErrorObject const error: KeywordErrorDefinition = { message: ({keyword, schemaCode}) => str`should be ${KWDs[keyword as Kwd].okStr} ${schemaCode}`, params: ({keyword, schemaCode}) => _`{comparison: ${KWDs[keyword as Kwd].okStr}, limit: ${schemaCode}}`, } export const formatLimitDefinition: CodeKeywordDefinition = { keyword: Object.keys(KWDs), type: "string", schemaType: "string", $data: true, error, code(cxt) { const {gen, data, schemaCode, keyword, it} = cxt const {opts, self} = it if (!opts.validateFormats) return const fCxt = new KeywordCxt(it, (self.RULES.all.format as Rule).definition, "format") if (fCxt.$data) validate$DataFormat() else validateFormat() function validate$DataFormat(): void { const fmts = gen.scopeValue("formats", { ref: self.formats, code: opts.code.formats, }) const fmt = gen.const("fmt", _`${fmts}[${fCxt.schemaCode}]`) cxt.fail$data( or( _`typeof ${fmt} != "object"`, _`${fmt} instanceof RegExp`, _`typeof ${fmt}.compare != "function"`, compareCode(fmt) ) ) } function validateFormat(): void { const format = fCxt.schema as string const fmtDef: AddedFormat | undefined = self.formats[format] if (!fmtDef || fmtDef === true) return if ( typeof fmtDef != "object" || fmtDef instanceof RegExp || typeof fmtDef.compare != "function" ) { throw new Error(`"${keyword}": format "${format}" does not define "compare" function`) } const fmt = gen.scopeValue("formats", { key: format, ref: fmtDef, code: opts.code.formats ? _`${opts.code.formats}${getProperty(format)}` : undefined, }) cxt.fail$data(compareCode(fmt)) } function compareCode(fmt: Name): Code { return _`${fmt}.compare(${data}, ${schemaCode}) ${KWDs[keyword as Kwd].fail} 0` } }, dependencies: ["format"], } const formatLimitPlugin: Plugin = (ajv: Ajv): Ajv => { ajv.addKeyword(formatLimitDefinition) return ajv } export default formatLimitPlugin