properties.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import type {CodeKeywordDefinition} from "../../types"
  2. import type KeywordCxt from "../../compile/context"
  3. import {propertyInData, allSchemaProperties, isOwnProperty} from "../code"
  4. import {alwaysValidSchema, schemaRefOrVal} from "../../compile/util"
  5. import {_, and, not, Code, Name} from "../../compile/codegen"
  6. import {checkMetadata} from "./metadata"
  7. import {checkNullableObject} from "./nullable"
  8. const def: CodeKeywordDefinition = {
  9. keyword: "properties",
  10. schemaType: "object",
  11. code: validateProperties,
  12. }
  13. export function validateProperties(cxt: KeywordCxt): void {
  14. checkMetadata(cxt)
  15. const {gen, data, parentSchema, it} = cxt
  16. const {additionalProperties, nullable} = parentSchema
  17. if (it.jtdDiscriminator && nullable) throw new Error("JTD: nullable inside discriminator mapping")
  18. if (commonProperties()) {
  19. throw new Error("JTD: properties and optionalProperties have common members")
  20. }
  21. const [allProps, properties] = schemaProperties("properties")
  22. const [allOptProps, optProperties] = schemaProperties("optionalProperties")
  23. if (properties.length === 0 && optProperties.length === 0 && additionalProperties) {
  24. return
  25. }
  26. const [valid, cond] =
  27. it.jtdDiscriminator === undefined
  28. ? checkNullableObject(cxt, data)
  29. : [gen.let("valid", false), true]
  30. gen.if(cond, () =>
  31. gen.assign(valid, true).block(() => {
  32. validateProps(properties, "properties", true)
  33. validateProps(optProperties, "optionalProperties")
  34. if (!additionalProperties) validateAdditional()
  35. })
  36. )
  37. cxt.pass(valid)
  38. function commonProperties(): boolean {
  39. const props = parentSchema.properties as Record<string, any> | undefined
  40. const optProps = parentSchema.optionalProperties as Record<string, any> | undefined
  41. if (!(props && optProps)) return false
  42. for (const p in props) {
  43. if (Object.prototype.hasOwnProperty.call(optProps, p)) return true
  44. }
  45. return false
  46. }
  47. function schemaProperties(keyword: string): [string[], string[]] {
  48. const schema = parentSchema[keyword]
  49. const allPs = schema ? allSchemaProperties(schema) : []
  50. if (it.jtdDiscriminator && allPs.some((p) => p === it.jtdDiscriminator)) {
  51. throw new Error(`JTD: discriminator tag used in ${keyword}`)
  52. }
  53. const ps = allPs.filter((p) => !alwaysValidSchema(it, schema[p]))
  54. return [allPs, ps]
  55. }
  56. function validateProps(props: string[], keyword: string, required?: boolean): void {
  57. const _valid = gen.var("valid")
  58. for (const prop of props) {
  59. gen.if(
  60. propertyInData(gen, data, prop, it.opts.ownProperties),
  61. () => applyPropertySchema(prop, keyword, _valid),
  62. missingProperty
  63. )
  64. cxt.ok(_valid)
  65. }
  66. function missingProperty(): void {
  67. if (required) {
  68. gen.assign(_valid, false)
  69. cxt.error()
  70. } else {
  71. gen.assign(_valid, true)
  72. }
  73. }
  74. }
  75. function applyPropertySchema(prop: string, keyword: string, _valid: Name): void {
  76. cxt.subschema(
  77. {
  78. keyword,
  79. schemaProp: prop,
  80. dataProp: prop,
  81. },
  82. _valid
  83. )
  84. }
  85. function validateAdditional(): void {
  86. gen.forIn("key", data, (key: Name) => {
  87. const _allProps =
  88. it.jtdDiscriminator === undefined ? allProps : [it.jtdDiscriminator].concat(allProps)
  89. const addProp = isAdditional(key, _allProps, "properties")
  90. const addOptProp = isAdditional(key, allOptProps, "optionalProperties")
  91. const extra =
  92. addProp === true ? addOptProp : addOptProp === true ? addProp : and(addProp, addOptProp)
  93. gen.if(extra, () => {
  94. if (it.opts.removeAdditional) {
  95. gen.code(_`delete ${data}[${key}]`)
  96. } else {
  97. // cxt.setParams({additionalProperty: key})
  98. cxt.error()
  99. if (!it.opts.allErrors) gen.break()
  100. }
  101. })
  102. })
  103. }
  104. function isAdditional(key: Name, props: string[], keyword: string): Code | true {
  105. let additional: Code | boolean
  106. if (props.length > 8) {
  107. // TODO maybe an option instead of hard-coded 8?
  108. const propsSchema = schemaRefOrVal(it, parentSchema[keyword], keyword)
  109. additional = not(isOwnProperty(gen, propsSchema as Code, key))
  110. } else if (props.length) {
  111. additional = and(...props.map((p) => _`${key} !== ${p}`))
  112. } else {
  113. additional = true
  114. }
  115. return additional
  116. }
  117. }
  118. export default def