index.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import type {
  2. AnySchema,
  3. AnySchemaObject,
  4. AnyValidateFunction,
  5. AsyncValidateFunction,
  6. EvaluatedProperties,
  7. EvaluatedItems,
  8. } from "../types"
  9. import type Ajv from "../core"
  10. import type {InstanceOptions} from "../core"
  11. import {CodeGen, _, nil, stringify, Name, Code, ValueScopeName} from "./codegen"
  12. import ValidationError from "../runtime/validation_error"
  13. import N from "./names"
  14. import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve"
  15. import {schemaHasRulesButRef, unescapeFragment} from "./util"
  16. import {validateFunctionCode} from "./validate"
  17. import {URIComponent} from "fast-uri"
  18. import {JSONType} from "./rules"
  19. export type SchemaRefs = {
  20. [Ref in string]?: SchemaEnv | AnySchema
  21. }
  22. export interface SchemaCxt {
  23. readonly gen: CodeGen
  24. readonly allErrors?: boolean // validation mode - whether to collect all errors or break on error
  25. readonly data: Name // Name with reference to the current part of data instance
  26. readonly parentData: Name // should be used in keywords modifying data
  27. readonly parentDataProperty: Code | number // should be used in keywords modifying data
  28. readonly dataNames: Name[]
  29. readonly dataPathArr: (Code | number)[]
  30. readonly dataLevel: number // the level of the currently validated data,
  31. // it can be used to access both the property names and the data on all levels from the top.
  32. dataTypes: JSONType[] // data types applied to the current part of data instance
  33. definedProperties: Set<string> // set of properties to keep track of for required checks
  34. readonly topSchemaRef: Code
  35. readonly validateName: Name
  36. evaluated?: Name
  37. readonly ValidationError?: Name
  38. readonly schema: AnySchema // current schema object - equal to parentSchema passed via KeywordCxt
  39. readonly schemaEnv: SchemaEnv
  40. readonly rootId: string
  41. baseId: string // the current schema base URI that should be used as the base for resolving URIs in references (\$ref)
  42. readonly schemaPath: Code // the run-time expression that evaluates to the property name of the current schema
  43. readonly errSchemaPath: string // this is actual string, should not be changed to Code
  44. readonly errorPath: Code
  45. readonly propertyName?: Name
  46. readonly compositeRule?: boolean // true indicates that the current schema is inside the compound keyword,
  47. // where failing some rule doesn't mean validation failure (`anyOf`, `oneOf`, `not`, `if`).
  48. // This flag is used to determine whether you can return validation result immediately after any error in case the option `allErrors` is not `true.
  49. // You only need to use it if you have many steps in your keywords and potentially can define multiple errors.
  50. props?: EvaluatedProperties | Name // properties evaluated by this schema - used by parent schema or assigned to validation function
  51. items?: EvaluatedItems | Name // last item evaluated by this schema - used by parent schema or assigned to validation function
  52. jtdDiscriminator?: string
  53. jtdMetadata?: boolean
  54. readonly createErrors?: boolean
  55. readonly opts: InstanceOptions // Ajv instance option.
  56. readonly self: Ajv // current Ajv instance
  57. }
  58. export interface SchemaObjCxt extends SchemaCxt {
  59. readonly schema: AnySchemaObject
  60. }
  61. interface SchemaEnvArgs {
  62. readonly schema: AnySchema
  63. readonly schemaId?: "$id" | "id"
  64. readonly root?: SchemaEnv
  65. readonly baseId?: string
  66. readonly schemaPath?: string
  67. readonly localRefs?: LocalRefs
  68. readonly meta?: boolean
  69. }
  70. export class SchemaEnv implements SchemaEnvArgs {
  71. readonly schema: AnySchema
  72. readonly schemaId?: "$id" | "id"
  73. readonly root: SchemaEnv
  74. baseId: string // TODO possibly, it should be readonly
  75. schemaPath?: string
  76. localRefs?: LocalRefs
  77. readonly meta?: boolean
  78. readonly $async?: boolean // true if the current schema is asynchronous.
  79. readonly refs: SchemaRefs = {}
  80. readonly dynamicAnchors: {[Ref in string]?: true} = {}
  81. validate?: AnyValidateFunction
  82. validateName?: ValueScopeName
  83. serialize?: (data: unknown) => string
  84. serializeName?: ValueScopeName
  85. parse?: (data: string) => unknown
  86. parseName?: ValueScopeName
  87. constructor(env: SchemaEnvArgs) {
  88. let schema: AnySchemaObject | undefined
  89. if (typeof env.schema == "object") schema = env.schema
  90. this.schema = env.schema
  91. this.schemaId = env.schemaId
  92. this.root = env.root || this
  93. this.baseId = env.baseId ?? normalizeId(schema?.[env.schemaId || "$id"])
  94. this.schemaPath = env.schemaPath
  95. this.localRefs = env.localRefs
  96. this.meta = env.meta
  97. this.$async = schema?.$async
  98. this.refs = {}
  99. }
  100. }
  101. // let codeSize = 0
  102. // let nodeCount = 0
  103. // Compiles schema in SchemaEnv
  104. export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
  105. // TODO refactor - remove compilations
  106. const _sch = getCompilingSchema.call(this, sch)
  107. if (_sch) return _sch
  108. const rootId = getFullPath(this.opts.uriResolver, sch.root.baseId) // TODO if getFullPath removed 1 tests fails
  109. const {es5, lines} = this.opts.code
  110. const {ownProperties} = this.opts
  111. const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
  112. let _ValidationError
  113. if (sch.$async) {
  114. _ValidationError = gen.scopeValue("Error", {
  115. ref: ValidationError,
  116. code: _`require("ajv/dist/runtime/validation_error").default`,
  117. })
  118. }
  119. const validateName = gen.scopeName("validate")
  120. sch.validateName = validateName
  121. const schemaCxt: SchemaCxt = {
  122. gen,
  123. allErrors: this.opts.allErrors,
  124. data: N.data,
  125. parentData: N.parentData,
  126. parentDataProperty: N.parentDataProperty,
  127. dataNames: [N.data],
  128. dataPathArr: [nil], // TODO can its length be used as dataLevel if nil is removed?
  129. dataLevel: 0,
  130. dataTypes: [],
  131. definedProperties: new Set<string>(),
  132. topSchemaRef: gen.scopeValue(
  133. "schema",
  134. this.opts.code.source === true
  135. ? {ref: sch.schema, code: stringify(sch.schema)}
  136. : {ref: sch.schema}
  137. ),
  138. validateName,
  139. ValidationError: _ValidationError,
  140. schema: sch.schema,
  141. schemaEnv: sch,
  142. rootId,
  143. baseId: sch.baseId || rootId,
  144. schemaPath: nil,
  145. errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"),
  146. errorPath: _`""`,
  147. opts: this.opts,
  148. self: this,
  149. }
  150. let sourceCode: string | undefined
  151. try {
  152. this._compilations.add(sch)
  153. validateFunctionCode(schemaCxt)
  154. gen.optimize(this.opts.code.optimize)
  155. // gen.optimize(1)
  156. const validateCode = gen.toString()
  157. sourceCode = `${gen.scopeRefs(N.scope)}return ${validateCode}`
  158. // console.log((codeSize += sourceCode.length), (nodeCount += gen.nodeCount))
  159. if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch)
  160. // console.log("\n\n\n *** \n", sourceCode)
  161. const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode)
  162. const validate: AnyValidateFunction = makeValidate(this, this.scope.get())
  163. this.scope.value(validateName, {ref: validate})
  164. validate.errors = null
  165. validate.schema = sch.schema
  166. validate.schemaEnv = sch
  167. if (sch.$async) (validate as AsyncValidateFunction).$async = true
  168. if (this.opts.code.source === true) {
  169. validate.source = {validateName, validateCode, scopeValues: gen._values}
  170. }
  171. if (this.opts.unevaluated) {
  172. const {props, items} = schemaCxt
  173. validate.evaluated = {
  174. props: props instanceof Name ? undefined : props,
  175. items: items instanceof Name ? undefined : items,
  176. dynamicProps: props instanceof Name,
  177. dynamicItems: items instanceof Name,
  178. }
  179. if (validate.source) validate.source.evaluated = stringify(validate.evaluated)
  180. }
  181. sch.validate = validate
  182. return sch
  183. } catch (e) {
  184. delete sch.validate
  185. delete sch.validateName
  186. if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode)
  187. // console.log("\n\n\n *** \n", sourceCode, this.opts)
  188. throw e
  189. } finally {
  190. this._compilations.delete(sch)
  191. }
  192. }
  193. export function resolveRef(
  194. this: Ajv,
  195. root: SchemaEnv,
  196. baseId: string,
  197. ref: string
  198. ): AnySchema | SchemaEnv | undefined {
  199. ref = resolveUrl(this.opts.uriResolver, baseId, ref)
  200. const schOrFunc = root.refs[ref]
  201. if (schOrFunc) return schOrFunc
  202. let _sch = resolve.call(this, root, ref)
  203. if (_sch === undefined) {
  204. const schema = root.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv
  205. const {schemaId} = this.opts
  206. if (schema) _sch = new SchemaEnv({schema, schemaId, root, baseId})
  207. }
  208. if (_sch === undefined) return
  209. return (root.refs[ref] = inlineOrCompile.call(this, _sch))
  210. }
  211. function inlineOrCompile(this: Ajv, sch: SchemaEnv): AnySchema | SchemaEnv {
  212. if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema
  213. return sch.validate ? sch : compileSchema.call(this, sch)
  214. }
  215. // Index of schema compilation in the currently compiled list
  216. export function getCompilingSchema(this: Ajv, schEnv: SchemaEnv): SchemaEnv | void {
  217. for (const sch of this._compilations) {
  218. if (sameSchemaEnv(sch, schEnv)) return sch
  219. }
  220. }
  221. function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean {
  222. return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId
  223. }
  224. // resolve and compile the references ($ref)
  225. // TODO returns AnySchemaObject (if the schema can be inlined) or validation function
  226. function resolve(
  227. this: Ajv,
  228. root: SchemaEnv, // information about the root schema for the current schema
  229. ref: string // reference to resolve
  230. ): SchemaEnv | undefined {
  231. let sch
  232. while (typeof (sch = this.refs[ref]) == "string") ref = sch
  233. return sch || this.schemas[ref] || resolveSchema.call(this, root, ref)
  234. }
  235. // Resolve schema, its root and baseId
  236. export function resolveSchema(
  237. this: Ajv,
  238. root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
  239. ref: string // reference to resolve
  240. ): SchemaEnv | undefined {
  241. const p = this.opts.uriResolver.parse(ref)
  242. const refPath = _getFullPath(this.opts.uriResolver, p)
  243. let baseId = getFullPath(this.opts.uriResolver, root.baseId, undefined)
  244. // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
  245. if (Object.keys(root.schema).length > 0 && refPath === baseId) {
  246. return getJsonPointer.call(this, p, root)
  247. }
  248. const id = normalizeId(refPath)
  249. const schOrRef = this.refs[id] || this.schemas[id]
  250. if (typeof schOrRef == "string") {
  251. const sch = resolveSchema.call(this, root, schOrRef)
  252. if (typeof sch?.schema !== "object") return
  253. return getJsonPointer.call(this, p, sch)
  254. }
  255. if (typeof schOrRef?.schema !== "object") return
  256. if (!schOrRef.validate) compileSchema.call(this, schOrRef)
  257. if (id === normalizeId(ref)) {
  258. const {schema} = schOrRef
  259. const {schemaId} = this.opts
  260. const schId = schema[schemaId]
  261. if (schId) baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
  262. return new SchemaEnv({schema, schemaId, root, baseId})
  263. }
  264. return getJsonPointer.call(this, p, schOrRef)
  265. }
  266. const PREVENT_SCOPE_CHANGE = new Set([
  267. "properties",
  268. "patternProperties",
  269. "enum",
  270. "dependencies",
  271. "definitions",
  272. ])
  273. function getJsonPointer(
  274. this: Ajv,
  275. parsedRef: URIComponent,
  276. {baseId, schema, root}: SchemaEnv
  277. ): SchemaEnv | undefined {
  278. if (parsedRef.fragment?.[0] !== "/") return
  279. for (const part of parsedRef.fragment.slice(1).split("/")) {
  280. if (typeof schema === "boolean") return
  281. const partSchema = schema[unescapeFragment(part)]
  282. if (partSchema === undefined) return
  283. schema = partSchema
  284. // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
  285. const schId = typeof schema === "object" && schema[this.opts.schemaId]
  286. if (!PREVENT_SCOPE_CHANGE.has(part) && schId) {
  287. baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
  288. }
  289. }
  290. let env: SchemaEnv | undefined
  291. if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
  292. const $ref = resolveUrl(this.opts.uriResolver, baseId, schema.$ref)
  293. env = resolveSchema.call(this, root, $ref)
  294. }
  295. // even though resolution failed we need to return SchemaEnv to throw exception
  296. // so that compileAsync loads missing schema.
  297. const {schemaId} = this.opts
  298. env = env || new SchemaEnv({schema, schemaId, root, baseId})
  299. if (env.schema !== env.root.schema) return env
  300. return undefined
  301. }