index.ts 4.0 KB

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