index.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import type {AnySchema} from "../../types"
  2. import type {SchemaCxt, SchemaObjCxt} from ".."
  3. import type {InstanceOptions} from "../../core"
  4. import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema"
  5. import {coerceAndCheckDataType, getSchemaTypes} from "./dataType"
  6. import {schemaKeywords} from "./iterate"
  7. import {_, nil, str, Block, Code, Name, CodeGen} from "../codegen"
  8. import N from "../names"
  9. import {resolveUrl} from "../resolve"
  10. import {schemaHasRulesButRef, checkUnknownRules} from "../util"
  11. // schema compilation - generates validation function, subschemaCode (below) is used for subschemas
  12. export function validateFunctionCode(it: SchemaCxt): void {
  13. if (isSchemaObj(it)) {
  14. checkKeywords(it)
  15. if (schemaCxtHasRules(it)) {
  16. topSchemaObjCode(it)
  17. return
  18. }
  19. }
  20. validateFunction(it, () => topBoolOrEmptySchema(it))
  21. }
  22. function validateFunction(
  23. {gen, validateName, schema, schemaEnv, opts}: SchemaCxt,
  24. body: Block
  25. ): void {
  26. if (opts.code.es5) {
  27. gen.func(validateName, _`${N.data}, ${N.valCxt}`, schemaEnv.$async, () => {
  28. gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`)
  29. destructureValCxtES5(gen, opts)
  30. gen.code(body)
  31. })
  32. } else {
  33. gen.func(validateName, _`${N.data}, ${destructureValCxt(opts)}`, schemaEnv.$async, () =>
  34. gen.code(funcSourceUrl(schema, opts)).code(body)
  35. )
  36. }
  37. }
  38. function destructureValCxt(opts: InstanceOptions): Code {
  39. return _`{${N.dataPath}="", ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}=${N.data}${
  40. opts.dynamicRef ? _`, ${N.dynamicAnchors}={}` : nil
  41. }}={}`
  42. }
  43. function destructureValCxtES5(gen: CodeGen, opts: InstanceOptions): void {
  44. gen.if(
  45. N.valCxt,
  46. () => {
  47. gen.var(N.dataPath, _`${N.valCxt}.${N.dataPath}`)
  48. gen.var(N.parentData, _`${N.valCxt}.${N.parentData}`)
  49. gen.var(N.parentDataProperty, _`${N.valCxt}.${N.parentDataProperty}`)
  50. gen.var(N.rootData, _`${N.valCxt}.${N.rootData}`)
  51. if (opts.dynamicRef) gen.var(N.dynamicAnchors, _`${N.valCxt}.${N.dynamicAnchors}`)
  52. },
  53. () => {
  54. gen.var(N.dataPath, _`""`)
  55. gen.var(N.parentData, _`undefined`)
  56. gen.var(N.parentDataProperty, _`undefined`)
  57. gen.var(N.rootData, N.data)
  58. if (opts.dynamicRef) gen.var(N.dynamicAnchors, _`{}`)
  59. }
  60. )
  61. }
  62. function topSchemaObjCode(it: SchemaObjCxt): void {
  63. const {schema, opts, gen} = it
  64. validateFunction(it, () => {
  65. if (opts.$comment && schema.$comment) commentKeyword(it)
  66. checkNoDefault(it)
  67. gen.let(N.vErrors, null)
  68. gen.let(N.errors, 0)
  69. if (opts.unevaluated) resetEvaluated(it)
  70. typeAndKeywords(it)
  71. returnResults(it)
  72. })
  73. return
  74. }
  75. function resetEvaluated(it: SchemaObjCxt): void {
  76. // TODO maybe some hook to execute it in the end to check whether props/items are Name, as in assignEvaluated
  77. const {gen, validateName} = it
  78. it.evaluated = gen.const("evaluated", _`${validateName}.evaluated`)
  79. gen.if(_`${it.evaluated}.dynamicProps`, () => gen.assign(_`${it.evaluated}.props`, _`undefined`))
  80. gen.if(_`${it.evaluated}.dynamicItems`, () => gen.assign(_`${it.evaluated}.items`, _`undefined`))
  81. }
  82. function funcSourceUrl(schema: AnySchema, opts: InstanceOptions): Code {
  83. return typeof schema == "object" && schema.$id && (opts.code.source || opts.code.process)
  84. ? _`/*# sourceURL=${schema.$id} */`
  85. : nil
  86. }
  87. // schema compilation - this function is used recursively to generate code for sub-schemas
  88. export function subschemaCode(it: SchemaCxt, valid: Name): void {
  89. if (isSchemaObj(it)) {
  90. checkKeywords(it)
  91. if (schemaCxtHasRules(it)) {
  92. subSchemaObjCode(it, valid)
  93. return
  94. }
  95. }
  96. boolOrEmptySchema(it, valid)
  97. }
  98. export function schemaCxtHasRules({schema, self}: SchemaCxt): boolean {
  99. if (typeof schema == "boolean") return !schema
  100. for (const key in schema) if (self.RULES.all[key]) return true
  101. return false
  102. }
  103. function isSchemaObj(it: SchemaCxt): it is SchemaObjCxt {
  104. return typeof it.schema != "boolean"
  105. }
  106. function subSchemaObjCode(it: SchemaObjCxt, valid: Name): void {
  107. const {schema, gen, opts} = it
  108. if (opts.$comment && schema.$comment) commentKeyword(it)
  109. updateContext(it)
  110. checkAsync(it)
  111. const errsCount = gen.const("_errs", N.errors)
  112. typeAndKeywords(it, errsCount)
  113. // TODO var
  114. gen.var(valid, _`${errsCount} === ${N.errors}`)
  115. }
  116. function checkKeywords(it: SchemaObjCxt): void {
  117. checkUnknownRules(it)
  118. checkRefsAndKeywords(it)
  119. }
  120. function typeAndKeywords(it: SchemaObjCxt, errsCount?: Name): void {
  121. const types = getSchemaTypes(it.schema)
  122. const checkedTypes = coerceAndCheckDataType(it, types)
  123. schemaKeywords(it, types, !checkedTypes, errsCount)
  124. }
  125. function checkRefsAndKeywords(it: SchemaObjCxt): void {
  126. const {schema, errSchemaPath, opts, self} = it
  127. if (schema.$ref && opts.ignoreKeywordsWithRef && schemaHasRulesButRef(schema, self.RULES)) {
  128. self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`)
  129. }
  130. }
  131. function checkNoDefault(it: SchemaObjCxt): void {
  132. const {schema, opts} = it
  133. if (schema.default !== undefined && opts.useDefaults && opts.strict) {
  134. checkStrictMode(it, "default is ignored in the schema root")
  135. }
  136. }
  137. function updateContext(it: SchemaObjCxt): void {
  138. if (it.schema.$id) it.baseId = resolveUrl(it.baseId, it.schema.$id)
  139. }
  140. function checkAsync(it: SchemaObjCxt): void {
  141. if (it.schema.$async && !it.schemaEnv.$async) throw new Error("async schema in sync schema")
  142. }
  143. function commentKeyword({gen, schemaEnv, schema, errSchemaPath, opts}: SchemaObjCxt): void {
  144. const msg = schema.$comment
  145. if (opts.$comment === true) {
  146. gen.code(_`${N.self}.logger.log(${msg})`)
  147. } else if (typeof opts.$comment == "function") {
  148. const schemaPath = str`${errSchemaPath}/$comment`
  149. const rootName = gen.scopeValue("root", {ref: schemaEnv.root})
  150. gen.code(_`${N.self}.opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`)
  151. }
  152. }
  153. function returnResults(it: SchemaCxt): void {
  154. const {gen, schemaEnv, validateName, ValidationError, opts} = it
  155. if (schemaEnv.$async) {
  156. // TODO assign unevaluated
  157. gen.if(
  158. _`${N.errors} === 0`,
  159. () => gen.return(N.data),
  160. () => gen.throw(_`new ${ValidationError as Name}(${N.vErrors})`)
  161. )
  162. } else {
  163. gen.assign(_`${validateName}.errors`, N.vErrors)
  164. if (opts.unevaluated) assignEvaluated(it)
  165. gen.return(_`${N.errors} === 0`)
  166. }
  167. }
  168. function assignEvaluated({gen, evaluated, props, items}: SchemaCxt): void {
  169. if (props instanceof Name) gen.assign(_`${evaluated}.props`, props)
  170. if (items instanceof Name) gen.assign(_`${evaluated}.items`, items)
  171. }
  172. export function checkStrictMode(it: SchemaCxt, msg: string, mode = it.opts.strict): void {
  173. if (!mode) return
  174. msg = `strict mode: ${msg}`
  175. if (mode === true) throw new Error(msg)
  176. it.self.logger.warn(msg)
  177. }