subschema.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import type {AnySchema} from "../types"
  2. import type {SchemaObjCxt, SchemaCxt} from "./index"
  3. import {subschemaCode} from "./validate"
  4. import {escapeFragment, escapeJsonPointer} from "./util"
  5. import {_, str, Code, Name, getProperty} from "./codegen"
  6. import {JSONType} from "./rules"
  7. interface SubschemaContext {
  8. // TODO use Optional? align with SchemCxt property types
  9. schema: AnySchema
  10. schemaPath: Code
  11. errSchemaPath: string
  12. topSchemaRef?: Code
  13. errorPath?: Code
  14. dataLevel?: number
  15. dataTypes?: JSONType[]
  16. data?: Name
  17. parentData?: Name
  18. parentDataProperty?: Code | number
  19. dataNames?: Name[]
  20. dataPathArr?: (Code | number)[]
  21. propertyName?: Name
  22. jtdDiscriminator?: string
  23. jtdMetadata?: boolean
  24. compositeRule?: true
  25. createErrors?: boolean
  26. allErrors?: boolean
  27. }
  28. export enum Type {
  29. Num,
  30. Str,
  31. }
  32. export type SubschemaArgs = Partial<{
  33. keyword: string
  34. schemaProp: string | number
  35. schema: AnySchema
  36. schemaPath: Code
  37. errSchemaPath: string
  38. topSchemaRef: Code
  39. data: Name | Code
  40. dataProp: Code | string | number
  41. dataTypes: JSONType[]
  42. definedProperties: Set<string>
  43. propertyName: Name
  44. dataPropType: Type
  45. jtdDiscriminator: string
  46. jtdMetadata: boolean
  47. compositeRule: true
  48. createErrors: boolean
  49. allErrors: boolean
  50. }>
  51. export function applySubschema(it: SchemaObjCxt, appl: SubschemaArgs, valid: Name): SchemaCxt {
  52. const subschema = getSubschema(it, appl)
  53. extendSubschemaData(subschema, it, appl)
  54. extendSubschemaMode(subschema, appl)
  55. const nextContext = {...it, ...subschema, items: undefined, props: undefined}
  56. subschemaCode(nextContext, valid)
  57. return nextContext
  58. }
  59. function getSubschema(
  60. it: SchemaObjCxt,
  61. {keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaArgs
  62. ): SubschemaContext {
  63. if (keyword !== undefined && schema !== undefined) {
  64. throw new Error('both "keyword" and "schema" passed, only one allowed')
  65. }
  66. if (keyword !== undefined) {
  67. const sch = it.schema[keyword]
  68. return schemaProp === undefined
  69. ? {
  70. schema: sch,
  71. schemaPath: _`${it.schemaPath}${getProperty(keyword)}`,
  72. errSchemaPath: `${it.errSchemaPath}/${keyword}`,
  73. }
  74. : {
  75. schema: sch[schemaProp],
  76. schemaPath: _`${it.schemaPath}${getProperty(keyword)}${getProperty(schemaProp)}`,
  77. errSchemaPath: `${it.errSchemaPath}/${keyword}/${escapeFragment(schemaProp)}`,
  78. }
  79. }
  80. if (schema !== undefined) {
  81. if (schemaPath === undefined || errSchemaPath === undefined || topSchemaRef === undefined) {
  82. throw new Error('"schemaPath", "errSchemaPath" and "topSchemaRef" are required with "schema"')
  83. }
  84. return {
  85. schema,
  86. schemaPath,
  87. topSchemaRef,
  88. errSchemaPath,
  89. }
  90. }
  91. throw new Error('either "keyword" or "schema" must be passed')
  92. }
  93. function extendSubschemaData(
  94. subschema: SubschemaContext,
  95. it: SchemaObjCxt,
  96. {dataProp, dataPropType: dpType, data, dataTypes, propertyName}: SubschemaArgs
  97. ): void {
  98. if (data !== undefined && dataProp !== undefined) {
  99. throw new Error('both "data" and "dataProp" passed, only one allowed')
  100. }
  101. const {gen} = it
  102. if (dataProp !== undefined) {
  103. const {errorPath, dataPathArr, opts} = it
  104. const nextData = gen.let("data", _`${it.data}${getProperty(dataProp)}`, true)
  105. dataContextProps(nextData)
  106. subschema.errorPath = str`${errorPath}${getErrorPath(dataProp, dpType, opts.jsPropertySyntax)}`
  107. subschema.parentDataProperty = _`${dataProp}`
  108. subschema.dataPathArr = [...dataPathArr, subschema.parentDataProperty]
  109. }
  110. if (data !== undefined) {
  111. const nextData = data instanceof Name ? data : gen.let("data", data, true) // replaceable if used once?
  112. dataContextProps(nextData)
  113. if (propertyName !== undefined) subschema.propertyName = propertyName
  114. // TODO something is possibly wrong here with not changing parentDataProperty and not appending dataPathArr
  115. }
  116. if (dataTypes) subschema.dataTypes = dataTypes
  117. function dataContextProps(_nextData: Name): void {
  118. subschema.data = _nextData
  119. subschema.dataLevel = it.dataLevel + 1
  120. subschema.dataTypes = []
  121. it.definedProperties = new Set<string>()
  122. subschema.parentData = it.data
  123. subschema.dataNames = [...it.dataNames, _nextData]
  124. }
  125. }
  126. function extendSubschemaMode(
  127. subschema: SubschemaContext,
  128. {jtdDiscriminator, jtdMetadata, compositeRule, createErrors, allErrors}: SubschemaArgs
  129. ): void {
  130. if (compositeRule !== undefined) subschema.compositeRule = compositeRule
  131. if (createErrors !== undefined) subschema.createErrors = createErrors
  132. if (allErrors !== undefined) subschema.allErrors = allErrors
  133. subschema.jtdDiscriminator = jtdDiscriminator // not inherited
  134. subschema.jtdMetadata = jtdMetadata // not inherited
  135. }
  136. function getErrorPath(
  137. dataProp: Name | string | number,
  138. dataPropType?: Type,
  139. jsPropertySyntax?: boolean
  140. ): Code | string {
  141. // let path
  142. if (dataProp instanceof Name) {
  143. const isNumber = dataPropType === Type.Num
  144. return jsPropertySyntax
  145. ? isNumber
  146. ? _`"[" + ${dataProp} + "]"`
  147. : _`"['" + ${dataProp} + "']"`
  148. : isNumber
  149. ? _`"/" + ${dataProp}`
  150. : _`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")` // TODO maybe use global escapePointer
  151. }
  152. return jsPropertySyntax ? getProperty(dataProp).toString() : "/" + escapeJsonPointer(dataProp)
  153. }