index.ts 4.2 KB

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