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
			| 
											3 years ago
										 | 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 | ||
|  | } |