@redocly/ajv
Version:
Another JSON Schema Validator
162 lines (139 loc) • 5.49 kB
text/typescript
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 {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
}
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