You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							325 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							325 lines
						
					
					
						
							12 KiB
						
					
					
				| import type {
 | |
|   AnySchema,
 | |
|   AnySchemaObject,
 | |
|   AnyValidateFunction,
 | |
|   AsyncValidateFunction,
 | |
|   EvaluatedProperties,
 | |
|   EvaluatedItems,
 | |
| } from "../types"
 | |
| import type Ajv from "../core"
 | |
| import type {InstanceOptions} from "../core"
 | |
| import {CodeGen, _, nil, stringify, Name, Code, ValueScopeName} from "./codegen"
 | |
| import ValidationError from "../runtime/validation_error"
 | |
| import N from "./names"
 | |
| import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve"
 | |
| import {schemaHasRulesButRef, unescapeFragment} from "./util"
 | |
| import {validateFunctionCode} from "./validate"
 | |
| import * as URI from "uri-js"
 | |
| import {JSONType} from "./rules"
 | |
| 
 | |
| export type SchemaRefs = {
 | |
|   [Ref in string]?: SchemaEnv | AnySchema
 | |
| }
 | |
| 
 | |
| export interface SchemaCxt {
 | |
|   readonly gen: CodeGen
 | |
|   readonly allErrors?: boolean // validation mode - whether to collect all errors or break on error
 | |
|   readonly data: Name // Name with reference to the current part of data instance
 | |
|   readonly parentData: Name // should be used in keywords modifying data
 | |
|   readonly parentDataProperty: Code | number // should be used in keywords modifying data
 | |
|   readonly dataNames: Name[]
 | |
|   readonly dataPathArr: (Code | number)[]
 | |
|   readonly dataLevel: number // the level of the currently validated data,
 | |
|   // it can be used to access both the property names and the data on all levels from the top.
 | |
|   dataTypes: JSONType[] // data types applied to the current part of data instance
 | |
|   definedProperties: Set<string> // set of properties to keep track of for required checks
 | |
|   readonly topSchemaRef: Code
 | |
|   readonly validateName: Name
 | |
|   evaluated?: Name
 | |
|   readonly ValidationError?: Name
 | |
|   readonly schema: AnySchema // current schema object - equal to parentSchema passed via KeywordCxt
 | |
|   readonly schemaEnv: SchemaEnv
 | |
|   readonly rootId: string
 | |
|   baseId: string // the current schema base URI that should be used as the base for resolving URIs in references (\$ref)
 | |
|   readonly schemaPath: Code // the run-time expression that evaluates to the property name of the current schema
 | |
|   readonly errSchemaPath: string // this is actual string, should not be changed to Code
 | |
|   readonly errorPath: Code
 | |
|   readonly propertyName?: Name
 | |
|   readonly compositeRule?: boolean // true indicates that the current schema is inside the compound keyword,
 | |
|   // where failing some rule doesn't mean validation failure (`anyOf`, `oneOf`, `not`, `if`).
 | |
|   // This flag is used to determine whether you can return validation result immediately after any error in case the option `allErrors` is not `true.
 | |
|   // You only need to use it if you have many steps in your keywords and potentially can define multiple errors.
 | |
|   props?: EvaluatedProperties | Name // properties evaluated by this schema - used by parent schema or assigned to validation function
 | |
|   items?: EvaluatedItems | Name // last item evaluated by this schema - used by parent schema or assigned to validation function
 | |
|   jtdDiscriminator?: string
 | |
|   jtdMetadata?: boolean
 | |
|   readonly createErrors?: boolean
 | |
|   readonly opts: InstanceOptions // Ajv instance option.
 | |
|   readonly self: Ajv // current Ajv instance
 | |
| }
 | |
| 
 | |
| export interface SchemaObjCxt extends SchemaCxt {
 | |
|   readonly schema: AnySchemaObject
 | |
| }
 | |
| interface SchemaEnvArgs {
 | |
|   readonly schema: AnySchema
 | |
|   readonly schemaId?: "$id" | "id"
 | |
|   readonly root?: SchemaEnv
 | |
|   readonly baseId?: string
 | |
|   readonly schemaPath?: string
 | |
|   readonly localRefs?: LocalRefs
 | |
|   readonly meta?: boolean
 | |
| }
 | |
| 
 | |
| export class SchemaEnv implements SchemaEnvArgs {
 | |
|   readonly schema: AnySchema
 | |
|   readonly schemaId?: "$id" | "id"
 | |
|   readonly root: SchemaEnv
 | |
|   baseId: string // TODO possibly, it should be readonly
 | |
|   schemaPath?: string
 | |
|   localRefs?: LocalRefs
 | |
|   readonly meta?: boolean
 | |
|   readonly $async?: boolean // true if the current schema is asynchronous.
 | |
|   readonly refs: SchemaRefs = {}
 | |
|   readonly dynamicAnchors: {[Ref in string]?: true} = {}
 | |
|   validate?: AnyValidateFunction
 | |
|   validateName?: ValueScopeName
 | |
|   serialize?: (data: unknown) => string
 | |
|   serializeName?: ValueScopeName
 | |
|   parse?: (data: string) => unknown
 | |
|   parseName?: ValueScopeName
 | |
| 
 | |
|   constructor(env: SchemaEnvArgs) {
 | |
|     let schema: AnySchemaObject | undefined
 | |
|     if (typeof env.schema == "object") schema = env.schema
 | |
|     this.schema = env.schema
 | |
|     this.schemaId = env.schemaId
 | |
|     this.root = env.root || this
 | |
|     this.baseId = env.baseId ?? normalizeId(schema?.[env.schemaId || "$id"])
 | |
|     this.schemaPath = env.schemaPath
 | |
|     this.localRefs = env.localRefs
 | |
|     this.meta = env.meta
 | |
|     this.$async = schema?.$async
 | |
|     this.refs = {}
 | |
|   }
 | |
| }
 | |
| 
 | |
| // let codeSize = 0
 | |
| // let nodeCount = 0
 | |
| 
 | |
| // Compiles schema in SchemaEnv
 | |
| export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
 | |
|   // TODO refactor - remove compilations
 | |
|   const _sch = getCompilingSchema.call(this, sch)
 | |
|   if (_sch) return _sch
 | |
|   const rootId = getFullPath(this.opts.uriResolver, sch.root.baseId) // TODO if getFullPath removed 1 tests fails
 | |
|   const {es5, lines} = this.opts.code
 | |
|   const {ownProperties} = this.opts
 | |
|   const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
 | |
|   let _ValidationError
 | |
|   if (sch.$async) {
 | |
|     _ValidationError = gen.scopeValue("Error", {
 | |
|       ref: ValidationError,
 | |
|       code: _`require("ajv/dist/runtime/validation_error").default`,
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   const validateName = gen.scopeName("validate")
 | |
|   sch.validateName = validateName
 | |
| 
 | |
|   const schemaCxt: SchemaCxt = {
 | |
|     gen,
 | |
|     allErrors: this.opts.allErrors,
 | |
|     data: N.data,
 | |
|     parentData: N.parentData,
 | |
|     parentDataProperty: N.parentDataProperty,
 | |
|     dataNames: [N.data],
 | |
|     dataPathArr: [nil], // TODO can its length be used as dataLevel if nil is removed?
 | |
|     dataLevel: 0,
 | |
|     dataTypes: [],
 | |
|     definedProperties: new Set<string>(),
 | |
|     topSchemaRef: gen.scopeValue(
 | |
|       "schema",
 | |
|       this.opts.code.source === true
 | |
|         ? {ref: sch.schema, code: stringify(sch.schema)}
 | |
|         : {ref: sch.schema}
 | |
|     ),
 | |
|     validateName,
 | |
|     ValidationError: _ValidationError,
 | |
|     schema: sch.schema,
 | |
|     schemaEnv: sch,
 | |
|     rootId,
 | |
|     baseId: sch.baseId || rootId,
 | |
|     schemaPath: nil,
 | |
|     errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"),
 | |
|     errorPath: _`""`,
 | |
|     opts: this.opts,
 | |
|     self: this,
 | |
|   }
 | |
| 
 | |
|   let sourceCode: string | undefined
 | |
|   try {
 | |
|     this._compilations.add(sch)
 | |
|     validateFunctionCode(schemaCxt)
 | |
|     gen.optimize(this.opts.code.optimize)
 | |
|     // gen.optimize(1)
 | |
|     const validateCode = gen.toString()
 | |
|     sourceCode = `${gen.scopeRefs(N.scope)}return ${validateCode}`
 | |
|     // console.log((codeSize += sourceCode.length), (nodeCount += gen.nodeCount))
 | |
|     if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch)
 | |
|     // console.log("\n\n\n *** \n", sourceCode)
 | |
|     const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode)
 | |
|     const validate: AnyValidateFunction = makeValidate(this, this.scope.get())
 | |
|     this.scope.value(validateName, {ref: validate})
 | |
| 
 | |
|     validate.errors = null
 | |
|     validate.schema = sch.schema
 | |
|     validate.schemaEnv = sch
 | |
|     if (sch.$async) (validate as AsyncValidateFunction).$async = true
 | |
|     if (this.opts.code.source === true) {
 | |
|       validate.source = {validateName, validateCode, scopeValues: gen._values}
 | |
|     }
 | |
|     if (this.opts.unevaluated) {
 | |
|       const {props, items} = schemaCxt
 | |
|       validate.evaluated = {
 | |
|         props: props instanceof Name ? undefined : props,
 | |
|         items: items instanceof Name ? undefined : items,
 | |
|         dynamicProps: props instanceof Name,
 | |
|         dynamicItems: items instanceof Name,
 | |
|       }
 | |
|       if (validate.source) validate.source.evaluated = stringify(validate.evaluated)
 | |
|     }
 | |
|     sch.validate = validate
 | |
|     return sch
 | |
|   } catch (e) {
 | |
|     delete sch.validate
 | |
|     delete sch.validateName
 | |
|     if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode)
 | |
|     // console.log("\n\n\n *** \n", sourceCode, this.opts)
 | |
|     throw e
 | |
|   } finally {
 | |
|     this._compilations.delete(sch)
 | |
|   }
 | |
| }
 | |
| 
 | |
| export function resolveRef(
 | |
|   this: Ajv,
 | |
|   root: SchemaEnv,
 | |
|   baseId: string,
 | |
|   ref: string
 | |
| ): AnySchema | SchemaEnv | undefined {
 | |
|   ref = resolveUrl(this.opts.uriResolver, baseId, ref)
 | |
|   const schOrFunc = root.refs[ref]
 | |
|   if (schOrFunc) return schOrFunc
 | |
| 
 | |
|   let _sch = resolve.call(this, root, ref)
 | |
|   if (_sch === undefined) {
 | |
|     const schema = root.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv
 | |
|     const {schemaId} = this.opts
 | |
|     if (schema) _sch = new SchemaEnv({schema, schemaId, root, baseId})
 | |
|   }
 | |
| 
 | |
|   if (_sch === undefined) return
 | |
|   return (root.refs[ref] = inlineOrCompile.call(this, _sch))
 | |
| }
 | |
| 
 | |
| function inlineOrCompile(this: Ajv, sch: SchemaEnv): AnySchema | SchemaEnv {
 | |
|   if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema
 | |
|   return sch.validate ? sch : compileSchema.call(this, sch)
 | |
| }
 | |
| 
 | |
| // Index of schema compilation in the currently compiled list
 | |
| export function getCompilingSchema(this: Ajv, schEnv: SchemaEnv): SchemaEnv | void {
 | |
|   for (const sch of this._compilations) {
 | |
|     if (sameSchemaEnv(sch, schEnv)) return sch
 | |
|   }
 | |
| }
 | |
| 
 | |
| function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean {
 | |
|   return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId
 | |
| }
 | |
| 
 | |
| // resolve and compile the references ($ref)
 | |
| // TODO returns AnySchemaObject (if the schema can be inlined) or validation function
 | |
| function resolve(
 | |
|   this: Ajv,
 | |
|   root: SchemaEnv, // information about the root schema for the current schema
 | |
|   ref: string // reference to resolve
 | |
| ): SchemaEnv | undefined {
 | |
|   let sch
 | |
|   while (typeof (sch = this.refs[ref]) == "string") ref = sch
 | |
|   return sch || this.schemas[ref] || resolveSchema.call(this, root, ref)
 | |
| }
 | |
| 
 | |
| // Resolve schema, its root and baseId
 | |
| export function resolveSchema(
 | |
|   this: Ajv,
 | |
|   root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
 | |
|   ref: string // reference to resolve
 | |
| ): SchemaEnv | undefined {
 | |
|   const p = this.opts.uriResolver.parse(ref)
 | |
|   const refPath = _getFullPath(this.opts.uriResolver, p)
 | |
|   let baseId = getFullPath(this.opts.uriResolver, root.baseId, undefined)
 | |
|   // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
 | |
|   if (Object.keys(root.schema).length > 0 && refPath === baseId) {
 | |
|     return getJsonPointer.call(this, p, root)
 | |
|   }
 | |
| 
 | |
|   const id = normalizeId(refPath)
 | |
|   const schOrRef = this.refs[id] || this.schemas[id]
 | |
|   if (typeof schOrRef == "string") {
 | |
|     const sch = resolveSchema.call(this, root, schOrRef)
 | |
|     if (typeof sch?.schema !== "object") return
 | |
|     return getJsonPointer.call(this, p, sch)
 | |
|   }
 | |
| 
 | |
|   if (typeof schOrRef?.schema !== "object") return
 | |
|   if (!schOrRef.validate) compileSchema.call(this, schOrRef)
 | |
|   if (id === normalizeId(ref)) {
 | |
|     const {schema} = schOrRef
 | |
|     const {schemaId} = this.opts
 | |
|     const schId = schema[schemaId]
 | |
|     if (schId) baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
 | |
|     return new SchemaEnv({schema, schemaId, root, baseId})
 | |
|   }
 | |
|   return getJsonPointer.call(this, p, schOrRef)
 | |
| }
 | |
| 
 | |
| const PREVENT_SCOPE_CHANGE = new Set([
 | |
|   "properties",
 | |
|   "patternProperties",
 | |
|   "enum",
 | |
|   "dependencies",
 | |
|   "definitions",
 | |
| ])
 | |
| 
 | |
| function getJsonPointer(
 | |
|   this: Ajv,
 | |
|   parsedRef: URI.URIComponents,
 | |
|   {baseId, schema, root}: SchemaEnv
 | |
| ): SchemaEnv | undefined {
 | |
|   if (parsedRef.fragment?.[0] !== "/") return
 | |
|   for (const part of parsedRef.fragment.slice(1).split("/")) {
 | |
|     if (typeof schema === "boolean") return
 | |
|     const partSchema = schema[unescapeFragment(part)]
 | |
|     if (partSchema === undefined) return
 | |
|     schema = partSchema
 | |
|     // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
 | |
|     const schId = typeof schema === "object" && schema[this.opts.schemaId]
 | |
|     if (!PREVENT_SCOPE_CHANGE.has(part) && schId) {
 | |
|       baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
 | |
|     }
 | |
|   }
 | |
|   let env: SchemaEnv | undefined
 | |
|   if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
 | |
|     const $ref = resolveUrl(this.opts.uriResolver, baseId, schema.$ref)
 | |
|     env = resolveSchema.call(this, root, $ref)
 | |
|   }
 | |
|   // even though resolution failed we need to return SchemaEnv to throw exception
 | |
|   // so that compileAsync loads missing schema.
 | |
|   const {schemaId} = this.opts
 | |
|   env = env || new SchemaEnv({schema, schemaId, root, baseId})
 | |
|   if (env.schema !== env.root.schema) return env
 | |
|   return undefined
 | |
| }
 |