| 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(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(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 = URI.parse(ref) |
| const refPath = _getFullPath(p) |
| let baseId = getFullPath(root.baseId) |
| // 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(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(baseId, schId) |
| } |
| } |
| let env: SchemaEnv | undefined |
| if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) { |
| const $ref = resolveUrl(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 |
| } |