keyword.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import type {KeywordCxt} from "."
  2. import type {
  3. AnySchema,
  4. SchemaValidateFunction,
  5. AnyValidateFunction,
  6. AddedKeywordDefinition,
  7. MacroKeywordDefinition,
  8. FuncKeywordDefinition,
  9. } from "../../types"
  10. import type {SchemaObjCxt} from ".."
  11. import {_, nil, not, stringify, Code, Name, CodeGen} from "../codegen"
  12. import N from "../names"
  13. import type {JSONType} from "../rules"
  14. import {callValidateCode} from "../../vocabularies/code"
  15. import {extendErrors} from "../errors"
  16. type KeywordCompilationResult = AnySchema | SchemaValidateFunction | AnyValidateFunction
  17. export function macroKeywordCode(cxt: KeywordCxt, def: MacroKeywordDefinition): void {
  18. const {gen, keyword, schema, parentSchema, it} = cxt
  19. const macroSchema = def.macro.call(it.self, schema, parentSchema, it)
  20. const schemaRef = useKeyword(gen, keyword, macroSchema)
  21. if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true)
  22. const valid = gen.name("valid")
  23. cxt.subschema(
  24. {
  25. schema: macroSchema,
  26. schemaPath: nil,
  27. errSchemaPath: `${it.errSchemaPath}/${keyword}`,
  28. topSchemaRef: schemaRef,
  29. compositeRule: true,
  30. },
  31. valid
  32. )
  33. cxt.pass(valid, () => cxt.error(true))
  34. }
  35. export function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void {
  36. const {gen, keyword, schema, parentSchema, $data, it} = cxt
  37. checkAsyncKeyword(it, def)
  38. const validate =
  39. !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate
  40. const validateRef = useKeyword(gen, keyword, validate)
  41. const valid = gen.let("valid")
  42. cxt.block$data(valid, validateKeyword)
  43. cxt.ok(def.valid ?? valid)
  44. function validateKeyword(): void {
  45. if (def.errors === false) {
  46. assignValid()
  47. if (def.modifying) modifyData(cxt)
  48. reportErrs(() => cxt.error())
  49. } else {
  50. const ruleErrs = def.async ? validateAsync() : validateSync()
  51. if (def.modifying) modifyData(cxt)
  52. reportErrs(() => addErrs(cxt, ruleErrs))
  53. }
  54. }
  55. function validateAsync(): Name {
  56. const ruleErrs = gen.let("ruleErrs", null)
  57. gen.try(
  58. () => assignValid(_`await `),
  59. (e) =>
  60. gen.assign(valid, false).if(
  61. _`${e} instanceof ${it.ValidationError as Name}`,
  62. () => gen.assign(ruleErrs, _`${e}.errors`),
  63. () => gen.throw(e)
  64. )
  65. )
  66. return ruleErrs
  67. }
  68. function validateSync(): Code {
  69. const validateErrs = _`${validateRef}.errors`
  70. gen.assign(validateErrs, null)
  71. assignValid(nil)
  72. return validateErrs
  73. }
  74. function assignValid(_await: Code = def.async ? _`await ` : nil): void {
  75. const passCxt = it.opts.passContext ? N.this : N.self
  76. const passSchema = !(("compile" in def && !$data) || def.schema === false)
  77. gen.assign(
  78. valid,
  79. _`${_await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`,
  80. def.modifying
  81. )
  82. }
  83. function reportErrs(errors: () => void): void {
  84. gen.if(not(def.valid ?? valid), errors)
  85. }
  86. }
  87. function modifyData(cxt: KeywordCxt): void {
  88. const {gen, data, it} = cxt
  89. gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`))
  90. }
  91. function addErrs(cxt: KeywordCxt, errs: Code): void {
  92. const {gen} = cxt
  93. gen.if(
  94. _`Array.isArray(${errs})`,
  95. () => {
  96. gen
  97. .assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`)
  98. .assign(N.errors, _`${N.vErrors}.length`)
  99. extendErrors(cxt)
  100. },
  101. () => cxt.error()
  102. )
  103. }
  104. function checkAsyncKeyword({schemaEnv}: SchemaObjCxt, def: FuncKeywordDefinition): void {
  105. if (def.async && !schemaEnv.$async) throw new Error("async keyword in sync schema")
  106. }
  107. function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name {
  108. if (result === undefined) throw new Error(`keyword "${keyword}" failed to compile`)
  109. return gen.scopeValue(
  110. "keyword",
  111. typeof result == "function" ? {ref: result} : {ref: result, code: stringify(result)}
  112. )
  113. }
  114. export function validSchemaType(
  115. schema: unknown,
  116. schemaType: JSONType[],
  117. allowUndefined = false
  118. ): boolean {
  119. // TODO add tests
  120. return (
  121. !schemaType.length ||
  122. schemaType.some((st) =>
  123. st === "array"
  124. ? Array.isArray(schema)
  125. : st === "object"
  126. ? schema && typeof schema == "object" && !Array.isArray(schema)
  127. : typeof schema == st || (allowUndefined && typeof schema == "undefined")
  128. )
  129. )
  130. }
  131. export function validateKeywordUsage(
  132. {schema, opts, self, errSchemaPath}: SchemaObjCxt,
  133. def: AddedKeywordDefinition,
  134. keyword: string
  135. ): void {
  136. /* istanbul ignore if */
  137. if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) {
  138. throw new Error("ajv implementation error")
  139. }
  140. const deps = def.dependencies
  141. if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) {
  142. throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`)
  143. }
  144. if (def.validateSchema) {
  145. const valid = def.validateSchema(schema[keyword])
  146. if (!valid) {
  147. const msg =
  148. `keyword "${keyword}" value is invalid at path "${errSchemaPath}": ` +
  149. self.errorsText(def.validateSchema.errors)
  150. if (opts.validateSchema === "log") self.logger.error(msg)
  151. else throw new Error(msg)
  152. }
  153. }
  154. }