import type {KeywordCxt} from "." import type { AnySchema, SchemaValidateFunction, AnyValidateFunction, AddedKeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, } from "../../types" import type {SchemaObjCxt} from ".." import {_, nil, not, stringify, Code, Name, CodeGen} from "../codegen" import N from "../names" import type {JSONType} from "../rules" import {callValidateCode} from "../../vocabularies/code" import {extendErrors} from "../errors" type KeywordCompilationResult = AnySchema | SchemaValidateFunction | AnyValidateFunction export function macroKeywordCode(cxt: KeywordCxt, def: MacroKeywordDefinition): void { const {gen, keyword, schema, parentSchema, it} = cxt const macroSchema = def.macro.call(it.self, schema, parentSchema, it) const schemaRef = useKeyword(gen, keyword, macroSchema) if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true) const valid = gen.name("valid") cxt.subschema( { schema: macroSchema, schemaPath: nil, errSchemaPath: `${it.errSchemaPath}/${keyword}`, topSchemaRef: schemaRef, compositeRule: true, }, valid ) cxt.pass(valid, () => cxt.error(true)) } export function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void { const {gen, keyword, schema, parentSchema, $data, it} = cxt checkAsyncKeyword(it, def) const validate = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate const validateRef = useKeyword(gen, keyword, validate) const valid = gen.let("valid") cxt.block$data(valid, validateKeyword) cxt.ok(def.valid ?? valid) function validateKeyword(): void { if (def.errors === false) { assignValid() if (def.modifying) modifyData(cxt) reportErrs(() => cxt.error()) } else { const ruleErrs = def.async ? validateAsync() : validateSync() if (def.modifying) modifyData(cxt) reportErrs(() => addErrs(cxt, ruleErrs)) } } function validateAsync(): Name { const ruleErrs = gen.let("ruleErrs", null) gen.try( () => assignValid(_`await `), (e) => gen.assign(valid, false).if( _`${e} instanceof ${it.ValidationError as Name}`, () => gen.assign(ruleErrs, _`${e}.errors`), () => gen.throw(e) ) ) return ruleErrs } function validateSync(): Code { const validateErrs = _`${validateRef}.errors` gen.assign(validateErrs, null) assignValid(nil) return validateErrs } function assignValid(_await: Code = def.async ? _`await ` : nil): void { const passCxt = it.opts.passContext ? N.this : N.self const passSchema = !(("compile" in def && !$data) || def.schema === false) gen.assign( valid, _`${_await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`, def.modifying ) } function reportErrs(errors: () => void): void { gen.if(not(def.valid ?? valid), errors) } } function modifyData(cxt: KeywordCxt): void { const {gen, data, it} = cxt gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`)) } function addErrs(cxt: KeywordCxt, errs: Code): void { const {gen} = cxt gen.if( _`Array.isArray(${errs})`, () => { gen .assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`) .assign(N.errors, _`${N.vErrors}.length`) extendErrors(cxt) }, () => cxt.error() ) } function checkAsyncKeyword({schemaEnv}: SchemaObjCxt, def: FuncKeywordDefinition): void { if (def.async && !schemaEnv.$async) throw new Error("async keyword in sync schema") } function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name { if (result === undefined) throw new Error(`keyword "${keyword}" failed to compile`) return gen.scopeValue( "keyword", typeof result == "function" ? {ref: result} : {ref: result, code: stringify(result)} ) } export function validSchemaType( schema: unknown, schemaType: JSONType[], allowUndefined = false ): boolean { // TODO add tests return ( !schemaType.length || schemaType.some((st) => st === "array" ? Array.isArray(schema) : st === "object" ? schema && typeof schema == "object" && !Array.isArray(schema) : typeof schema == st || (allowUndefined && typeof schema == "undefined") ) ) } export function validateKeywordUsage( {schema, opts, self, errSchemaPath}: SchemaObjCxt, def: AddedKeywordDefinition, keyword: string ): void { /* istanbul ignore if */ if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { throw new Error("ajv implementation error") } const deps = def.dependencies if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) { throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) } if (def.validateSchema) { const valid = def.validateSchema(schema[keyword]) if (!valid) { const msg = `keyword "${keyword}" value is invalid at path "${errSchemaPath}": ` + self.errorsText(def.validateSchema.errors) if (opts.validateSchema === "log") self.logger.error(msg) else throw new Error(msg) } } }