2023-10-03 11:14:36 +08:00
|
|
|
import type {AnySchema} from "../../types"
|
|
|
|
import type {SchemaCxt, SchemaObjCxt} from ".."
|
|
|
|
import type {InstanceOptions} from "../../core"
|
|
|
|
import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema"
|
|
|
|
import {coerceAndCheckDataType, getSchemaTypes} from "./dataType"
|
|
|
|
import {schemaKeywords} from "./iterate"
|
|
|
|
import {_, nil, str, Block, Code, Name, CodeGen} from "../codegen"
|
|
|
|
import N from "../names"
|
|
|
|
import {resolveUrl} from "../resolve"
|
|
|
|
import {schemaHasRulesButRef, checkUnknownRules} from "../util"
|
|
|
|
|
|
|
|
// schema compilation - generates validation function, subschemaCode (below) is used for subschemas
|
|
|
|
export function validateFunctionCode(it: SchemaCxt): void {
|
|
|
|
if (isSchemaObj(it)) {
|
|
|
|
checkKeywords(it)
|
|
|
|
if (schemaCxtHasRules(it)) {
|
|
|
|
topSchemaObjCode(it)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
validateFunction(it, () => topBoolOrEmptySchema(it))
|
|
|
|
}
|
|
|
|
|
|
|
|
function validateFunction(
|
|
|
|
{gen, validateName, schema, schemaEnv, opts}: SchemaCxt,
|
|
|
|
body: Block
|
|
|
|
): void {
|
|
|
|
if (opts.code.es5) {
|
|
|
|
gen.func(validateName, _`${N.data}, ${N.valCxt}`, schemaEnv.$async, () => {
|
|
|
|
gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`)
|
|
|
|
destructureValCxtES5(gen, opts)
|
|
|
|
gen.code(body)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
gen.func(validateName, _`${N.data}, ${destructureValCxt(opts)}`, schemaEnv.$async, () =>
|
|
|
|
gen.code(funcSourceUrl(schema, opts)).code(body)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function destructureValCxt(opts: InstanceOptions): Code {
|
|
|
|
return _`{${N.dataPath}="", ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}=${N.data}${
|
|
|
|
opts.dynamicRef ? _`, ${N.dynamicAnchors}={}` : nil
|
|
|
|
}}={}`
|
|
|
|
}
|
|
|
|
|
|
|
|
function destructureValCxtES5(gen: CodeGen, opts: InstanceOptions): void {
|
|
|
|
gen.if(
|
|
|
|
N.valCxt,
|
|
|
|
() => {
|
|
|
|
gen.var(N.dataPath, _`${N.valCxt}.${N.dataPath}`)
|
|
|
|
gen.var(N.parentData, _`${N.valCxt}.${N.parentData}`)
|
|
|
|
gen.var(N.parentDataProperty, _`${N.valCxt}.${N.parentDataProperty}`)
|
|
|
|
gen.var(N.rootData, _`${N.valCxt}.${N.rootData}`)
|
|
|
|
if (opts.dynamicRef) gen.var(N.dynamicAnchors, _`${N.valCxt}.${N.dynamicAnchors}`)
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
gen.var(N.dataPath, _`""`)
|
|
|
|
gen.var(N.parentData, _`undefined`)
|
|
|
|
gen.var(N.parentDataProperty, _`undefined`)
|
|
|
|
gen.var(N.rootData, N.data)
|
|
|
|
if (opts.dynamicRef) gen.var(N.dynamicAnchors, _`{}`)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function topSchemaObjCode(it: SchemaObjCxt): void {
|
|
|
|
const {schema, opts, gen} = it
|
|
|
|
validateFunction(it, () => {
|
|
|
|
if (opts.$comment && schema.$comment) commentKeyword(it)
|
|
|
|
checkNoDefault(it)
|
|
|
|
gen.let(N.vErrors, null)
|
|
|
|
gen.let(N.errors, 0)
|
|
|
|
if (opts.unevaluated) resetEvaluated(it)
|
|
|
|
typeAndKeywords(it)
|
|
|
|
returnResults(it)
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
function resetEvaluated(it: SchemaObjCxt): void {
|
|
|
|
// TODO maybe some hook to execute it in the end to check whether props/items are Name, as in assignEvaluated
|
|
|
|
const {gen, validateName} = it
|
|
|
|
it.evaluated = gen.const("evaluated", _`${validateName}.evaluated`)
|
|
|
|
gen.if(_`${it.evaluated}.dynamicProps`, () => gen.assign(_`${it.evaluated}.props`, _`undefined`))
|
|
|
|
gen.if(_`${it.evaluated}.dynamicItems`, () => gen.assign(_`${it.evaluated}.items`, _`undefined`))
|
|
|
|
}
|
|
|
|
|
|
|
|
function funcSourceUrl(schema: AnySchema, opts: InstanceOptions): Code {
|
|
|
|
return typeof schema == "object" && schema.$id && (opts.code.source || opts.code.process)
|
|
|
|
? _`/*# sourceURL=${schema.$id} */`
|
|
|
|
: nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// schema compilation - this function is used recursively to generate code for sub-schemas
|
|
|
|
export function subschemaCode(it: SchemaCxt, valid: Name): void {
|
|
|
|
if (isSchemaObj(it)) {
|
|
|
|
checkKeywords(it)
|
|
|
|
if (schemaCxtHasRules(it)) {
|
|
|
|
subSchemaObjCode(it, valid)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
boolOrEmptySchema(it, valid)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function schemaCxtHasRules({schema, self}: SchemaCxt): boolean {
|
|
|
|
if (typeof schema == "boolean") return !schema
|
|
|
|
for (const key in schema) if (self.RULES.all[key]) return true
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
function isSchemaObj(it: SchemaCxt): it is SchemaObjCxt {
|
|
|
|
return typeof it.schema != "boolean"
|
|
|
|
}
|
|
|
|
|
|
|
|
function subSchemaObjCode(it: SchemaObjCxt, valid: Name): void {
|
|
|
|
const {schema, gen, opts} = it
|
|
|
|
if (opts.$comment && schema.$comment) commentKeyword(it)
|
|
|
|
updateContext(it)
|
|
|
|
checkAsync(it)
|
|
|
|
const errsCount = gen.const("_errs", N.errors)
|
|
|
|
typeAndKeywords(it, errsCount)
|
|
|
|
// TODO var
|
|
|
|
gen.var(valid, _`${errsCount} === ${N.errors}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkKeywords(it: SchemaObjCxt): void {
|
|
|
|
checkUnknownRules(it)
|
|
|
|
checkRefsAndKeywords(it)
|
|
|
|
}
|
|
|
|
|
|
|
|
function typeAndKeywords(it: SchemaObjCxt, errsCount?: Name): void {
|
|
|
|
if (it.opts.jtd) return schemaKeywords(it, [], false, errsCount)
|
|
|
|
const types = getSchemaTypes(it.schema)
|
|
|
|
const checkedTypes = coerceAndCheckDataType(it, types)
|
|
|
|
schemaKeywords(it, types, !checkedTypes, errsCount)
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkRefsAndKeywords(it: SchemaObjCxt): void {
|
|
|
|
const {schema, errSchemaPath, opts, self} = it
|
|
|
|
if (schema.$ref && opts.ignoreKeywordsWithRef && schemaHasRulesButRef(schema, self.RULES)) {
|
|
|
|
self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkNoDefault(it: SchemaObjCxt): void {
|
|
|
|
const {schema, opts} = it
|
|
|
|
if (schema.default !== undefined && opts.useDefaults && opts.strict) {
|
|
|
|
checkStrictMode(it, "default is ignored in the schema root")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateContext(it: SchemaObjCxt): void {
|
|
|
|
if (it.schema.$id) it.baseId = resolveUrl(it.baseId, it.schema.$id)
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkAsync(it: SchemaObjCxt): void {
|
|
|
|
if (it.schema.$async && !it.schemaEnv.$async) throw new Error("async schema in sync schema")
|
|
|
|
}
|
|
|
|
|
|
|
|
function commentKeyword({gen, schemaEnv, schema, errSchemaPath, opts}: SchemaObjCxt): void {
|
|
|
|
const msg = schema.$comment
|
|
|
|
if (opts.$comment === true) {
|
|
|
|
gen.code(_`${N.self}.logger.log(${msg})`)
|
|
|
|
} else if (typeof opts.$comment == "function") {
|
|
|
|
const schemaPath = str`${errSchemaPath}/$comment`
|
|
|
|
const rootName = gen.scopeValue("root", {ref: schemaEnv.root})
|
|
|
|
gen.code(_`${N.self}.opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function returnResults(it: SchemaCxt): void {
|
|
|
|
const {gen, schemaEnv, validateName, ValidationError, opts} = it
|
|
|
|
if (schemaEnv.$async) {
|
|
|
|
// TODO assign unevaluated
|
|
|
|
gen.if(
|
|
|
|
_`${N.errors} === 0`,
|
|
|
|
() => gen.return(N.data),
|
|
|
|
() => gen.throw(_`new ${ValidationError as Name}(${N.vErrors})`)
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
gen.assign(_`${validateName}.errors`, N.vErrors)
|
|
|
|
if (opts.unevaluated) assignEvaluated(it)
|
|
|
|
gen.return(_`${N.errors} === 0`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function assignEvaluated({gen, evaluated, props, items}: SchemaCxt): void {
|
|
|
|
if (props instanceof Name) gen.assign(_`${evaluated}.props`, props)
|
|
|
|
if (items instanceof Name) gen.assign(_`${evaluated}.items`, items)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function checkStrictMode(it: SchemaCxt, msg: string, mode = it.opts.strict): void {
|
|
|
|
if (!mode) return
|
|
|
|
msg = `strict mode: ${msg}`
|
|
|
|
if (mode === true) throw new Error(msg)
|
|
|
|
it.self.logger.warn(msg)
|
|
|
|
}
|