[79a0317] | 1 | import type {KeywordCxt} from "."
|
---|
| 2 | import type {
|
---|
| 3 | AnySchema,
|
---|
| 4 | SchemaValidateFunction,
|
---|
| 5 | AnyValidateFunction,
|
---|
| 6 | AddedKeywordDefinition,
|
---|
| 7 | MacroKeywordDefinition,
|
---|
| 8 | FuncKeywordDefinition,
|
---|
| 9 | } from "../../types"
|
---|
| 10 | import type {SchemaObjCxt} from ".."
|
---|
| 11 | import {_, nil, not, stringify, Code, Name, CodeGen} from "../codegen"
|
---|
| 12 | import N from "../names"
|
---|
| 13 | import type {JSONType} from "../rules"
|
---|
| 14 | import {callValidateCode} from "../../vocabularies/code"
|
---|
| 15 | import {extendErrors} from "../errors"
|
---|
| 16 |
|
---|
| 17 | type KeywordCompilationResult = AnySchema | SchemaValidateFunction | AnyValidateFunction
|
---|
| 18 |
|
---|
| 19 | export function macroKeywordCode(cxt: KeywordCxt, def: MacroKeywordDefinition): void {
|
---|
| 20 | const {gen, keyword, schema, parentSchema, it} = cxt
|
---|
| 21 | const macroSchema = def.macro.call(it.self, schema, parentSchema, it)
|
---|
| 22 | const schemaRef = useKeyword(gen, keyword, macroSchema)
|
---|
| 23 | if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true)
|
---|
| 24 |
|
---|
| 25 | const valid = gen.name("valid")
|
---|
| 26 | cxt.subschema(
|
---|
| 27 | {
|
---|
| 28 | schema: macroSchema,
|
---|
| 29 | schemaPath: nil,
|
---|
| 30 | errSchemaPath: `${it.errSchemaPath}/${keyword}`,
|
---|
| 31 | topSchemaRef: schemaRef,
|
---|
| 32 | compositeRule: true,
|
---|
| 33 | },
|
---|
| 34 | valid
|
---|
| 35 | )
|
---|
| 36 | cxt.pass(valid, () => cxt.error(true))
|
---|
| 37 | }
|
---|
| 38 |
|
---|
| 39 | export function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void {
|
---|
| 40 | const {gen, keyword, schema, parentSchema, $data, it} = cxt
|
---|
| 41 | checkAsyncKeyword(it, def)
|
---|
| 42 | const validate =
|
---|
| 43 | !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate
|
---|
| 44 | const validateRef = useKeyword(gen, keyword, validate)
|
---|
| 45 | const valid = gen.let("valid")
|
---|
| 46 | cxt.block$data(valid, validateKeyword)
|
---|
| 47 | cxt.ok(def.valid ?? valid)
|
---|
| 48 |
|
---|
| 49 | function validateKeyword(): void {
|
---|
| 50 | if (def.errors === false) {
|
---|
| 51 | assignValid()
|
---|
| 52 | if (def.modifying) modifyData(cxt)
|
---|
| 53 | reportErrs(() => cxt.error())
|
---|
| 54 | } else {
|
---|
| 55 | const ruleErrs = def.async ? validateAsync() : validateSync()
|
---|
| 56 | if (def.modifying) modifyData(cxt)
|
---|
| 57 | reportErrs(() => addErrs(cxt, ruleErrs))
|
---|
| 58 | }
|
---|
| 59 | }
|
---|
| 60 |
|
---|
| 61 | function validateAsync(): Name {
|
---|
| 62 | const ruleErrs = gen.let("ruleErrs", null)
|
---|
| 63 | gen.try(
|
---|
| 64 | () => assignValid(_`await `),
|
---|
| 65 | (e) =>
|
---|
| 66 | gen.assign(valid, false).if(
|
---|
| 67 | _`${e} instanceof ${it.ValidationError as Name}`,
|
---|
| 68 | () => gen.assign(ruleErrs, _`${e}.errors`),
|
---|
| 69 | () => gen.throw(e)
|
---|
| 70 | )
|
---|
| 71 | )
|
---|
| 72 | return ruleErrs
|
---|
| 73 | }
|
---|
| 74 |
|
---|
| 75 | function validateSync(): Code {
|
---|
| 76 | const validateErrs = _`${validateRef}.errors`
|
---|
| 77 | gen.assign(validateErrs, null)
|
---|
| 78 | assignValid(nil)
|
---|
| 79 | return validateErrs
|
---|
| 80 | }
|
---|
| 81 |
|
---|
| 82 | function assignValid(_await: Code = def.async ? _`await ` : nil): void {
|
---|
| 83 | const passCxt = it.opts.passContext ? N.this : N.self
|
---|
| 84 | const passSchema = !(("compile" in def && !$data) || def.schema === false)
|
---|
| 85 | gen.assign(
|
---|
| 86 | valid,
|
---|
| 87 | _`${_await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`,
|
---|
| 88 | def.modifying
|
---|
| 89 | )
|
---|
| 90 | }
|
---|
| 91 |
|
---|
| 92 | function reportErrs(errors: () => void): void {
|
---|
| 93 | gen.if(not(def.valid ?? valid), errors)
|
---|
| 94 | }
|
---|
| 95 | }
|
---|
| 96 |
|
---|
| 97 | function modifyData(cxt: KeywordCxt): void {
|
---|
| 98 | const {gen, data, it} = cxt
|
---|
| 99 | gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`))
|
---|
| 100 | }
|
---|
| 101 |
|
---|
| 102 | function addErrs(cxt: KeywordCxt, errs: Code): void {
|
---|
| 103 | const {gen} = cxt
|
---|
| 104 | gen.if(
|
---|
| 105 | _`Array.isArray(${errs})`,
|
---|
| 106 | () => {
|
---|
| 107 | gen
|
---|
| 108 | .assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`)
|
---|
| 109 | .assign(N.errors, _`${N.vErrors}.length`)
|
---|
| 110 | extendErrors(cxt)
|
---|
| 111 | },
|
---|
| 112 | () => cxt.error()
|
---|
| 113 | )
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | function checkAsyncKeyword({schemaEnv}: SchemaObjCxt, def: FuncKeywordDefinition): void {
|
---|
| 117 | if (def.async && !schemaEnv.$async) throw new Error("async keyword in sync schema")
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name {
|
---|
| 121 | if (result === undefined) throw new Error(`keyword "${keyword}" failed to compile`)
|
---|
| 122 | return gen.scopeValue(
|
---|
| 123 | "keyword",
|
---|
| 124 | typeof result == "function" ? {ref: result} : {ref: result, code: stringify(result)}
|
---|
| 125 | )
|
---|
| 126 | }
|
---|
| 127 |
|
---|
| 128 | export function validSchemaType(
|
---|
| 129 | schema: unknown,
|
---|
| 130 | schemaType: JSONType[],
|
---|
| 131 | allowUndefined = false
|
---|
| 132 | ): boolean {
|
---|
| 133 | // TODO add tests
|
---|
| 134 | return (
|
---|
| 135 | !schemaType.length ||
|
---|
| 136 | schemaType.some((st) =>
|
---|
| 137 | st === "array"
|
---|
| 138 | ? Array.isArray(schema)
|
---|
| 139 | : st === "object"
|
---|
| 140 | ? schema && typeof schema == "object" && !Array.isArray(schema)
|
---|
| 141 | : typeof schema == st || (allowUndefined && typeof schema == "undefined")
|
---|
| 142 | )
|
---|
| 143 | )
|
---|
| 144 | }
|
---|
| 145 |
|
---|
| 146 | export function validateKeywordUsage(
|
---|
| 147 | {schema, opts, self, errSchemaPath}: SchemaObjCxt,
|
---|
| 148 | def: AddedKeywordDefinition,
|
---|
| 149 | keyword: string
|
---|
| 150 | ): void {
|
---|
| 151 | /* istanbul ignore if */
|
---|
| 152 | if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) {
|
---|
| 153 | throw new Error("ajv implementation error")
|
---|
| 154 | }
|
---|
| 155 |
|
---|
| 156 | const deps = def.dependencies
|
---|
| 157 | if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) {
|
---|
| 158 | throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`)
|
---|
| 159 | }
|
---|
| 160 |
|
---|
| 161 | if (def.validateSchema) {
|
---|
| 162 | const valid = def.validateSchema(schema[keyword])
|
---|
| 163 | if (!valid) {
|
---|
| 164 | const msg =
|
---|
| 165 | `keyword "${keyword}" value is invalid at path "${errSchemaPath}": ` +
|
---|
| 166 | self.errorsText(def.validateSchema.errors)
|
---|
| 167 | if (opts.validateSchema === "log") self.logger.error(msg)
|
---|
| 168 | else throw new Error(msg)
|
---|
| 169 | }
|
---|
| 170 | }
|
---|
| 171 | }
|
---|