properties.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import type {
  2. CodeKeywordDefinition,
  3. ErrorObject,
  4. KeywordErrorDefinition,
  5. SchemaObject,
  6. } from "../../types"
  7. import type {KeywordCxt} from "../../compile/validate"
  8. import {propertyInData, allSchemaProperties, isOwnProperty} from "../code"
  9. import {alwaysValidSchema, schemaRefOrVal} from "../../compile/util"
  10. import {_, and, not, Code, Name} from "../../compile/codegen"
  11. import {checkMetadata} from "./metadata"
  12. import {checkNullableObject} from "./nullable"
  13. import {typeErrorMessage, typeErrorParams, _JTDTypeError} from "./error"
  14. enum PropError {
  15. Additional = "additional",
  16. Missing = "missing",
  17. }
  18. type PropKeyword = "properties" | "optionalProperties"
  19. type PropSchema = {[P in string]?: SchemaObject}
  20. export type JTDPropertiesError =
  21. | _JTDTypeError<PropKeyword, "object", PropSchema>
  22. | ErrorObject<PropKeyword, {error: PropError.Additional; additionalProperty: string}, PropSchema>
  23. | ErrorObject<PropKeyword, {error: PropError.Missing; missingProperty: string}, PropSchema>
  24. export const error: KeywordErrorDefinition = {
  25. message: (cxt) => {
  26. const {params} = cxt
  27. return params.propError
  28. ? params.propError === PropError.Additional
  29. ? "must NOT have additional properties"
  30. : `must have property '${params.missingProperty}'`
  31. : typeErrorMessage(cxt, "object")
  32. },
  33. params: (cxt) => {
  34. const {params} = cxt
  35. return params.propError
  36. ? params.propError === PropError.Additional
  37. ? _`{error: ${params.propError}, additionalProperty: ${params.additionalProperty}}`
  38. : _`{error: ${params.propError}, missingProperty: ${params.missingProperty}}`
  39. : typeErrorParams(cxt, "object")
  40. },
  41. }
  42. const def: CodeKeywordDefinition = {
  43. keyword: "properties",
  44. schemaType: "object",
  45. error,
  46. code: validateProperties,
  47. }
  48. // const error: KeywordErrorDefinition = {
  49. // message: "should NOT have additional properties",
  50. // params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`,
  51. // }
  52. export function validateProperties(cxt: KeywordCxt): void {
  53. checkMetadata(cxt)
  54. const {gen, data, parentSchema, it} = cxt
  55. const {additionalProperties, nullable} = parentSchema
  56. if (it.jtdDiscriminator && nullable) throw new Error("JTD: nullable inside discriminator mapping")
  57. if (commonProperties()) {
  58. throw new Error("JTD: properties and optionalProperties have common members")
  59. }
  60. const [allProps, properties] = schemaProperties("properties")
  61. const [allOptProps, optProperties] = schemaProperties("optionalProperties")
  62. if (properties.length === 0 && optProperties.length === 0 && additionalProperties) {
  63. return
  64. }
  65. const [valid, cond] =
  66. it.jtdDiscriminator === undefined
  67. ? checkNullableObject(cxt, data)
  68. : [gen.let("valid", false), true]
  69. gen.if(cond, () =>
  70. gen.assign(valid, true).block(() => {
  71. validateProps(properties, "properties", true)
  72. validateProps(optProperties, "optionalProperties")
  73. if (!additionalProperties) validateAdditional()
  74. })
  75. )
  76. cxt.pass(valid)
  77. function commonProperties(): boolean {
  78. const props = parentSchema.properties as Record<string, any> | undefined
  79. const optProps = parentSchema.optionalProperties as Record<string, any> | undefined
  80. if (!(props && optProps)) return false
  81. for (const p in props) {
  82. if (Object.prototype.hasOwnProperty.call(optProps, p)) return true
  83. }
  84. return false
  85. }
  86. function schemaProperties(keyword: string): [string[], string[]] {
  87. const schema = parentSchema[keyword]
  88. const allPs = schema ? allSchemaProperties(schema) : []
  89. if (it.jtdDiscriminator && allPs.some((p) => p === it.jtdDiscriminator)) {
  90. throw new Error(`JTD: discriminator tag used in ${keyword}`)
  91. }
  92. const ps = allPs.filter((p) => !alwaysValidSchema(it, schema[p]))
  93. return [allPs, ps]
  94. }
  95. function validateProps(props: string[], keyword: string, required?: boolean): void {
  96. const _valid = gen.var("valid")
  97. for (const prop of props) {
  98. gen.if(
  99. propertyInData(gen, data, prop, it.opts.ownProperties),
  100. () => applyPropertySchema(prop, keyword, _valid),
  101. () => missingProperty(prop)
  102. )
  103. cxt.ok(_valid)
  104. }
  105. function missingProperty(prop: string): void {
  106. if (required) {
  107. gen.assign(_valid, false)
  108. cxt.error(false, {propError: PropError.Missing, missingProperty: prop}, {schemaPath: prop})
  109. } else {
  110. gen.assign(_valid, true)
  111. }
  112. }
  113. }
  114. function applyPropertySchema(prop: string, keyword: string, _valid: Name): void {
  115. cxt.subschema(
  116. {
  117. keyword,
  118. schemaProp: prop,
  119. dataProp: prop,
  120. },
  121. _valid
  122. )
  123. }
  124. function validateAdditional(): void {
  125. gen.forIn("key", data, (key: Name) => {
  126. const addProp = isAdditional(key, allProps, "properties", it.jtdDiscriminator)
  127. const addOptProp = isAdditional(key, allOptProps, "optionalProperties")
  128. const extra =
  129. addProp === true ? addOptProp : addOptProp === true ? addProp : and(addProp, addOptProp)
  130. gen.if(extra, () => {
  131. if (it.opts.removeAdditional) {
  132. gen.code(_`delete ${data}[${key}]`)
  133. } else {
  134. cxt.error(
  135. false,
  136. {propError: PropError.Additional, additionalProperty: key},
  137. {instancePath: key, parentSchema: true}
  138. )
  139. if (!it.opts.allErrors) gen.break()
  140. }
  141. })
  142. })
  143. }
  144. function isAdditional(
  145. key: Name,
  146. props: string[],
  147. keyword: string,
  148. jtdDiscriminator?: string
  149. ): Code | true {
  150. let additional: Code | boolean
  151. if (props.length > 8) {
  152. // TODO maybe an option instead of hard-coded 8?
  153. const propsSchema = schemaRefOrVal(it, parentSchema[keyword], keyword)
  154. additional = not(isOwnProperty(gen, propsSchema as Code, key))
  155. if (jtdDiscriminator !== undefined) {
  156. additional = and(additional, _`${key} !== ${jtdDiscriminator}`)
  157. }
  158. } else if (props.length || jtdDiscriminator !== undefined) {
  159. const ps = jtdDiscriminator === undefined ? props : [jtdDiscriminator].concat(props)
  160. additional = and(...ps.map((p) => _`${key} !== ${p}`))
  161. } else {
  162. additional = true
  163. }
  164. return additional
  165. }
  166. }
  167. export default def