164 lines
5.5 KiB
Plaintext
164 lines
5.5 KiB
Plaintext
import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} from "../../types"
|
|
import type {KeywordCxt} from "../../compile/validate"
|
|
import {_, getProperty, Name} from "../../compile/codegen"
|
|
import {DiscrError, DiscrErrorObj} from "../discriminator/types"
|
|
import {resolveRef, SchemaEnv} from "../../compile"
|
|
import MissingRefError from "../../compile/ref_error"
|
|
import {schemaHasRulesButRef} from "../../compile/util"
|
|
|
|
export type DiscriminatorError = DiscrErrorObj<DiscrError.Tag> | DiscrErrorObj<DiscrError.Mapping>
|
|
|
|
const error: KeywordErrorDefinition = {
|
|
message: ({params: {discrError, tagName}}) =>
|
|
discrError === DiscrError.Tag
|
|
? `tag "${tagName}" must be string`
|
|
: `value of tag "${tagName}" must be in oneOf or anyOf`,
|
|
params: ({params: {discrError, tag, tagName}}) =>
|
|
_`{error: ${discrError}, tag: ${tagName}, tagValue: ${tag}}`,
|
|
}
|
|
|
|
function getDiscriminatorPropertyFromAllOf(
|
|
sch: AnySchemaObject,
|
|
tagName: string
|
|
): AnySchemaObject | undefined {
|
|
if (!sch.allOf || !Array.isArray(sch.allOf)) {
|
|
return undefined
|
|
}
|
|
|
|
for (const subschema of sch.allOf) {
|
|
if (subschema?.properties?.[tagName]) {
|
|
return subschema.properties[tagName] as AnySchemaObject
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
const def: CodeKeywordDefinition = {
|
|
keyword: "discriminator",
|
|
type: "object",
|
|
schemaType: "object",
|
|
error,
|
|
code(cxt: KeywordCxt) {
|
|
const {gen, data, schema, parentSchema, it} = cxt
|
|
|
|
const keyword = parentSchema.oneOf ? "oneOf" : parentSchema.anyOf ? "anyOf" : undefined
|
|
|
|
if (!it.opts.discriminator) {
|
|
throw new Error("discriminator: requires discriminator option")
|
|
}
|
|
const tagName = schema.propertyName
|
|
if (typeof tagName != "string") throw new Error("discriminator: requires propertyName")
|
|
if (!keyword) throw new Error("discriminator: requires oneOf or anyOf composite keyword")
|
|
const parentSchemaVariants = parentSchema[keyword]
|
|
const valid = gen.let("valid", false)
|
|
const tag = gen.const("tag", _`${data}${getProperty(tagName)}`)
|
|
gen.if(
|
|
_`typeof ${tag} == "string"`,
|
|
() => validateMapping(),
|
|
() => cxt.error(false, {discrError: DiscrError.Tag, tag, tagName})
|
|
)
|
|
cxt.ok(valid)
|
|
|
|
function validateMapping(): void {
|
|
const mapping = getMapping()
|
|
gen.if(false)
|
|
for (const tagValue in mapping) {
|
|
gen.elseIf(_`${tag} === ${tagValue}`)
|
|
gen.assign(valid, applyTagSchema(mapping[tagValue]))
|
|
}
|
|
gen.else()
|
|
cxt.error(false, {discrError: DiscrError.Mapping, tag, tagName})
|
|
gen.endIf()
|
|
}
|
|
|
|
function applyTagSchema(schemaProp?: number): Name {
|
|
const _valid = gen.name("valid")
|
|
const schCxt = cxt.subschema({keyword, schemaProp}, _valid)
|
|
cxt.mergeEvaluated(schCxt, Name)
|
|
return _valid
|
|
}
|
|
|
|
function getMapping(): {[T in string]?: number} {
|
|
const discriminatorMapping: {[T in string]?: number} = {}
|
|
const topRequired = hasRequired(parentSchema)
|
|
let tagRequired = true
|
|
for (let i = 0; i < parentSchemaVariants.length; i++) {
|
|
let sch = parentSchemaVariants[i]
|
|
const schRef = sch?.$ref
|
|
|
|
if (schRef && schema.mapping) {
|
|
const {mapping} = schema
|
|
const matchedKeys = Object.keys(mapping).filter((key) => mapping[key] === sch.$ref)
|
|
|
|
if (matchedKeys.length) {
|
|
for (const key of matchedKeys) {
|
|
addMapping(key, i)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (schRef && !schemaHasRulesButRef(sch, it.self.RULES)) {
|
|
sch = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, schRef)
|
|
if (sch instanceof SchemaEnv) sch = sch.schema
|
|
if (sch === undefined) throw new MissingRefError(it.opts.uriResolver, it.baseId, schRef)
|
|
}
|
|
|
|
let propSch = sch?.properties?.[tagName]
|
|
if (!propSch && sch?.allOf) {
|
|
propSch = getDiscriminatorPropertyFromAllOf(sch, tagName)
|
|
}
|
|
|
|
if (typeof propSch != "object") {
|
|
throw new Error(
|
|
`discriminator: ${keyword} subschemas (or referenced schemas) must have "properties/${tagName}" or match mapping`
|
|
)
|
|
}
|
|
tagRequired = tagRequired && (topRequired || hasRequired(sch))
|
|
addMappings(propSch, i)
|
|
}
|
|
if (!tagRequired) throw new Error(`discriminator: "${tagName}" must be required`)
|
|
return discriminatorMapping
|
|
|
|
function hasRequired(sch: AnySchemaObject): boolean {
|
|
if (Array.isArray(sch.required) && sch.required.includes(tagName)) {
|
|
return true
|
|
}
|
|
|
|
if (sch.allOf && Array.isArray(sch.allOf)) {
|
|
for (const subschema of sch.allOf) {
|
|
const subSch = subschema as AnySchemaObject
|
|
if (Array.isArray(subSch.required) && subSch.required.includes(tagName)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function addMappings(sch: AnySchemaObject, i: number): void {
|
|
if (sch.const) {
|
|
addMapping(sch.const, i)
|
|
} else if (sch.enum) {
|
|
for (const tagValue of sch.enum) {
|
|
addMapping(tagValue, i)
|
|
}
|
|
} else {
|
|
throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`)
|
|
}
|
|
}
|
|
|
|
function addMapping(tagValue: unknown, i: number): void {
|
|
if (typeof tagValue != "string" || tagValue in discriminatorMapping) {
|
|
throw new Error(`discriminator: "${tagName}" values must be unique strings`)
|
|
}
|
|
discriminatorMapping[tagValue] = i
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
export default def
|