keyword.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import type {
  2. AddedKeywordDefinition,
  3. MacroKeywordDefinition,
  4. FuncKeywordDefinition,
  5. AnySchema,
  6. SchemaValidateFunction,
  7. AnyValidateFunction,
  8. } from "../../types"
  9. import type {SchemaObjCxt} from ".."
  10. import type {JSONType} from "../rules"
  11. import KeywordCxt from "../context"
  12. import {extendErrors} from "../errors"
  13. import {callValidateCode} from "../../vocabularies/code"
  14. import {CodeGen, _, nil, not, stringify, Code, Name} from "../codegen"
  15. import N from "../names"
  16. type KeywordCompilationResult = AnySchema | SchemaValidateFunction | AnyValidateFunction
  17. export function keywordCode(
  18. it: SchemaObjCxt,
  19. keyword: string,
  20. def: AddedKeywordDefinition,
  21. ruleType?: JSONType
  22. ): void {
  23. const cxt = new KeywordCxt(it, def, keyword)
  24. if ("code" in def) {
  25. def.code(cxt, ruleType)
  26. } else if (cxt.$data && def.validate) {
  27. funcKeywordCode(cxt, def)
  28. } else if ("macro" in def) {
  29. macroKeywordCode(cxt, def)
  30. } else if (def.compile || def.validate) {
  31. funcKeywordCode(cxt, def)
  32. }
  33. }
  34. function macroKeywordCode(cxt: KeywordCxt, def: MacroKeywordDefinition): void {
  35. const {gen, keyword, schema, parentSchema, it} = cxt
  36. const macroSchema = def.macro.call(it.self, schema, parentSchema, it)
  37. const schemaRef = useKeyword(gen, keyword, macroSchema)
  38. if (it.opts.validateSchema !== false) it.self.validateSchema(macroSchema, true)
  39. const valid = gen.name("valid")
  40. cxt.subschema(
  41. {
  42. schema: macroSchema,
  43. schemaPath: nil,
  44. errSchemaPath: `${it.errSchemaPath}/${keyword}`,
  45. topSchemaRef: schemaRef,
  46. compositeRule: true,
  47. },
  48. valid
  49. )
  50. cxt.pass(valid, () => cxt.error(true))
  51. }
  52. function funcKeywordCode(cxt: KeywordCxt, def: FuncKeywordDefinition): void {
  53. const {gen, keyword, schema, parentSchema, $data, it} = cxt
  54. checkAsync(it, def)
  55. const validate =
  56. !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate
  57. const validateRef = useKeyword(gen, keyword, validate)
  58. const valid = gen.let("valid")
  59. cxt.block$data(valid, validateKeyword)
  60. cxt.ok(def.valid ?? valid)
  61. function validateKeyword(): void {
  62. if (def.errors === false) {
  63. assignValid()
  64. if (def.modifying) modifyData(cxt)
  65. reportErrs(() => cxt.error())
  66. } else {
  67. const ruleErrs = def.async ? validateAsync() : validateSync()
  68. if (def.modifying) modifyData(cxt)
  69. reportErrs(() => addErrs(cxt, ruleErrs))
  70. }
  71. }
  72. function validateAsync(): Name {
  73. const ruleErrs = gen.let("ruleErrs", null)
  74. gen.try(
  75. () => assignValid(_`await `),
  76. (e) =>
  77. gen.assign(valid, false).if(
  78. _`${e} instanceof ${it.ValidationError as Name}`,
  79. () => gen.assign(ruleErrs, _`${e}.errors`),
  80. () => gen.throw(e)
  81. )
  82. )
  83. return ruleErrs
  84. }
  85. function validateSync(): Code {
  86. const validateErrs = _`${validateRef}.errors`
  87. gen.assign(validateErrs, null)
  88. assignValid(nil)
  89. return validateErrs
  90. }
  91. function assignValid(_await: Code = def.async ? _`await ` : nil): void {
  92. const passCxt = it.opts.passContext ? N.this : N.self
  93. const passSchema = !(("compile" in def && !$data) || def.schema === false)
  94. gen.assign(
  95. valid,
  96. _`${_await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`,
  97. def.modifying
  98. )
  99. }
  100. function reportErrs(errors: () => void): void {
  101. gen.if(not(def.valid ?? valid), errors)
  102. }
  103. }
  104. function modifyData(cxt: KeywordCxt): void {
  105. const {gen, data, it} = cxt
  106. gen.if(it.parentData, () => gen.assign(data, _`${it.parentData}[${it.parentDataProperty}]`))
  107. }
  108. function addErrs(cxt: KeywordCxt, errs: Code): void {
  109. const {gen} = cxt
  110. gen.if(
  111. _`Array.isArray(${errs})`,
  112. () => {
  113. gen
  114. .assign(N.vErrors, _`${N.vErrors} === null ? ${errs} : ${N.vErrors}.concat(${errs})`)
  115. .assign(N.errors, _`${N.vErrors}.length`)
  116. extendErrors(cxt)
  117. },
  118. () => cxt.error()
  119. )
  120. }
  121. function checkAsync({schemaEnv}: SchemaObjCxt, def: FuncKeywordDefinition): void {
  122. if (def.async && !schemaEnv.$async) throw new Error("async keyword in sync schema")
  123. }
  124. function useKeyword(gen: CodeGen, keyword: string, result?: KeywordCompilationResult): Name {
  125. if (result === undefined) throw new Error(`keyword "${keyword}" failed to compile`)
  126. return gen.scopeValue(
  127. "keyword",
  128. typeof result == "function" ? {ref: result} : {ref: result, code: stringify(result)}
  129. )
  130. }