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