UNPKG

4.12 kBPlain TextView Raw
1import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} from "../../types"
2import type {KeywordCxt} from "../../compile/validate"
3import {_, getProperty, Name} from "../../compile/codegen"
4import {DiscrError, DiscrErrorObj} from "../discriminator/types"
5import {resolveRef, SchemaEnv} from "../../compile"
6import {schemaHasRulesButRef} from "../../compile/util"
7
8export type DiscriminatorError = DiscrErrorObj<DiscrError.Tag> | DiscrErrorObj<DiscrError.Mapping>
9
10const error: KeywordErrorDefinition = {
11 message: ({params: {discrError, tagName}}) =>
12 discrError === DiscrError.Tag
13 ? `tag "${tagName}" must be string`
14 : `value of tag "${tagName}" must be in oneOf`,
15 params: ({params: {discrError, tag, tagName}}) =>
16 _`{error: ${discrError}, tag: ${tagName}, tagValue: ${tag}}`,
17}
18
19const def: CodeKeywordDefinition = {
20 keyword: "discriminator",
21 type: "object",
22 schemaType: "object",
23 error,
24 code(cxt: KeywordCxt) {
25 const {gen, data, schema, parentSchema, it} = cxt
26 const {oneOf} = parentSchema
27 if (!it.opts.discriminator) {
28 throw new Error("discriminator: requires discriminator option")
29 }
30 const tagName = schema.propertyName
31 if (typeof tagName != "string") throw new Error("discriminator: requires propertyName")
32 if (schema.mapping) throw new Error("discriminator: mapping is not supported")
33 if (!oneOf) throw new Error("discriminator: requires oneOf keyword")
34 const valid = gen.let("valid", false)
35 const tag = gen.const("tag", _`${data}${getProperty(tagName)}`)
36 gen.if(
37 _`typeof ${tag} == "string"`,
38 () => validateMapping(),
39 () => cxt.error(false, {discrError: DiscrError.Tag, tag, tagName})
40 )
41 cxt.ok(valid)
42
43 function validateMapping(): void {
44 const mapping = getMapping()
45 gen.if(false)
46 for (const tagValue in mapping) {
47 gen.elseIf(_`${tag} === ${tagValue}`)
48 gen.assign(valid, applyTagSchema(mapping[tagValue]))
49 }
50 gen.else()
51 cxt.error(false, {discrError: DiscrError.Mapping, tag, tagName})
52 gen.endIf()
53 }
54
55 function applyTagSchema(schemaProp?: number): Name {
56 const _valid = gen.name("valid")
57 const schCxt = cxt.subschema({keyword: "oneOf", schemaProp}, _valid)
58 cxt.mergeEvaluated(schCxt, Name)
59 return _valid
60 }
61
62 function getMapping(): {[T in string]?: number} {
63 const oneOfMapping: {[T in string]?: number} = {}
64 const topRequired = hasRequired(parentSchema)
65 let tagRequired = true
66 for (let i = 0; i < oneOf.length; i++) {
67 let sch = oneOf[i]
68 if (sch?.$ref && !schemaHasRulesButRef(sch, it.self.RULES)) {
69 sch = resolveRef.call(it.self, it.schemaEnv, it.baseId, sch?.$ref)
70 if (sch instanceof SchemaEnv) sch = sch.schema
71 }
72 const propSch = sch?.properties?.[tagName]
73 if (typeof propSch != "object") {
74 throw new Error(
75 `discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`
76 )
77 }
78 tagRequired = tagRequired && (topRequired || hasRequired(sch))
79 addMappings(propSch, i)
80 }
81 if (!tagRequired) throw new Error(`discriminator: "${tagName}" must be required`)
82 return oneOfMapping
83
84 function hasRequired({required}: AnySchemaObject): boolean {
85 return Array.isArray(required) && required.includes(tagName)
86 }
87
88 function addMappings(sch: AnySchemaObject, i: number): void {
89 if (sch.const) {
90 addMapping(sch.const, i)
91 } else if (sch.enum) {
92 for (const tagValue of sch.enum) {
93 addMapping(tagValue, i)
94 }
95 } else {
96 throw new Error(`discriminator: "properties/${tagName}" must have "const" or "enum"`)
97 }
98 }
99
100 function addMapping(tagValue: unknown, i: number): void {
101 if (typeof tagValue != "string" || tagValue in oneOfMapping) {
102 throw new Error(`discriminator: "${tagName}" values must be unique strings`)
103 }
104 oneOfMapping[tagValue] = i
105 }
106 }
107 },
108}
109
110export default def