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"
20 export type SchemaRefs = {
21 [Ref in string]?: SchemaEnv | AnySchema
24 export interface SchemaCxt {
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
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
56 readonly createErrors?: boolean
57 readonly opts: InstanceOptions // Ajv instance option.
58 readonly self: Ajv // current Ajv instance
61 export interface SchemaObjCxt extends SchemaCxt {
62 readonly schema: AnySchemaObject
64 interface SchemaEnvArgs {
65 readonly schema: AnySchema
66 readonly root?: SchemaEnv
67 readonly baseId?: string
68 readonly schemaPath?: string
69 readonly localRefs?: LocalRefs
70 readonly meta?: boolean
73 export class SchemaEnv implements SchemaEnvArgs {
74 readonly schema: AnySchema
75 readonly root: SchemaEnv
76 baseId: string // TODO possibly, it should be readonly
79 readonly meta?: boolean
80 readonly $async?: boolean // true if the current schema is asynchronous.
81 readonly refs: SchemaRefs = {}
82 readonly dynamicAnchors: {[Ref in string]?: true} = {}
83 validate?: AnyValidateFunction
84 validateName?: ValueScopeName
85 serialize?: (data: unknown) => string
86 serializeName?: ValueScopeName
87 parse?: (data: string) => unknown
88 parseName?: ValueScopeName
90 constructor(env: SchemaEnvArgs) {
91 let schema: AnySchemaObject | undefined
92 if (typeof env.schema == "object") schema = env.schema
93 this.schema = env.schema
94 this.root = env.root || this
95 this.baseId = env.baseId ?? normalizeId(schema?.$id)
96 this.schemaPath = env.schemaPath
97 this.localRefs = env.localRefs
99 this.$async = schema?.$async
107 // Compiles schema in SchemaEnv
108 export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
109 // TODO refactor - remove compilations
110 const _sch = getCompilingSchema.call(this, sch)
111 if (_sch) return _sch
112 const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails
113 const {es5, lines} = this.opts.code
114 const {ownProperties} = this.opts
115 const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
118 _ValidationError = gen.scopeValue("Error", {
119 ref: ValidationError,
120 code: _`require("ajv/dist/runtime/validation_error").default`,
124 const validateName = gen.scopeName("validate")
125 sch.validateName = validateName
127 const schemaCxt: SchemaCxt = {
129 allErrors: this.opts.allErrors,
131 parentData: N.parentData,
132 parentDataProperty: N.parentDataProperty,
134 dataPathArr: [nil], // TODO can its length be used as dataLevel if nil is removed?
137 definedProperties: new Set<string>(),
138 topSchemaRef: gen.scopeValue(
140 this.opts.code.source === true
141 ? {ref: sch.schema, code: stringify(sch.schema)}
145 ValidationError: _ValidationError,
149 baseId: sch.baseId || rootId,
151 errSchemaPath: sch.schemaPath || (this.opts.jtd ? "" : "#"),
157 let sourceCode: string | undefined
159 this._compilations.add(sch)
160 validateFunctionCode(schemaCxt)
161 gen.optimize(this.opts.code.optimize)
163 const validateCode = gen.toString()
164 sourceCode = `${gen.scopeRefs(N.scope)}return ${validateCode}`
165 // console.log((codeSize += sourceCode.length), (nodeCount += gen.nodeCount))
166 if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch)
167 // console.log("\n\n\n *** \n", sourceCode)
168 const makeValidate = new Function(`${N.self}`, `${N.scope}`, sourceCode)
169 const validate: AnyValidateFunction = makeValidate(this, this.scope.get())
170 this.scope.value(validateName, {ref: validate})
172 validate.errors = null
173 validate.schema = sch.schema
174 validate.schemaEnv = sch
175 if (sch.$async) (validate as AsyncValidateFunction).$async = true
176 if (this.opts.code.source === true) {
177 validate.source = {validateName, validateCode, scopeValues: gen._values}
179 if (this.opts.unevaluated) {
180 const {props, items} = schemaCxt
181 validate.evaluated = {
182 props: props instanceof Name ? undefined : props,
183 items: items instanceof Name ? undefined : items,
184 dynamicProps: props instanceof Name,
185 dynamicItems: items instanceof Name,
187 if (validate.source) validate.source.evaluated = stringify(validate.evaluated)
189 sch.validate = validate
193 delete sch.validateName
194 if (sourceCode) this.logger.error("Error compiling schema, function code:", sourceCode)
195 // console.log("\n\n\n *** \n", sourceCode, this.opts)
198 this._compilations.delete(sch)
202 export function resolveRef(
207 ): AnySchema | SchemaEnv | undefined {
208 ref = resolveUrl(baseId, ref)
209 const schOrFunc = root.refs[ref]
210 if (schOrFunc) return schOrFunc
212 let _sch = resolve.call(this, root, ref)
213 if (_sch === undefined) {
214 const schema = root.localRefs?.[ref] // TODO maybe localRefs should hold SchemaEnv
215 if (schema) _sch = new SchemaEnv({schema, root, baseId})
218 if (_sch === undefined) return
219 return (root.refs[ref] = inlineOrCompile.call(this, _sch))
222 function inlineOrCompile(this: Ajv, sch: SchemaEnv): AnySchema | SchemaEnv {
223 if (inlineRef(sch.schema, this.opts.inlineRefs)) return sch.schema
224 return sch.validate ? sch : compileSchema.call(this, sch)
227 // Index of schema compilation in the currently compiled list
228 export function getCompilingSchema(this: Ajv, schEnv: SchemaEnv): SchemaEnv | void {
229 for (const sch of this._compilations) {
230 if (sameSchemaEnv(sch, schEnv)) return sch
234 function sameSchemaEnv(s1: SchemaEnv, s2: SchemaEnv): boolean {
235 return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId
238 // resolve and compile the references ($ref)
239 // TODO returns AnySchemaObject (if the schema can be inlined) or validation function
242 root: SchemaEnv, // information about the root schema for the current schema
243 ref: string // reference to resolve
244 ): SchemaEnv | undefined {
246 while (typeof (sch = this.refs[ref]) == "string") ref = sch
247 return sch || this.schemas[ref] || resolveSchema.call(this, root, ref)
250 // Resolve schema, its root and baseId
251 export function resolveSchema(
253 root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
254 ref: string // reference to resolve
255 ): SchemaEnv | undefined {
256 const p = URI.parse(ref)
257 const refPath = _getFullPath(p)
258 let baseId = getFullPath(root.baseId)
259 // TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
260 if (Object.keys(root.schema).length > 0 && refPath === baseId) {
261 return getJsonPointer.call(this, p, root)
264 const id = normalizeId(refPath)
265 const schOrRef = this.refs[id] || this.schemas[id]
266 if (typeof schOrRef == "string") {
267 const sch = resolveSchema.call(this, root, schOrRef)
268 if (typeof sch?.schema !== "object") return
269 return getJsonPointer.call(this, p, sch)
272 if (typeof schOrRef?.schema !== "object") return
273 if (!schOrRef.validate) compileSchema.call(this, schOrRef)
274 if (id === normalizeId(ref)) {
275 const {schema} = schOrRef
276 if (schema.$id) baseId = resolveUrl(baseId, schema.$id)
277 return new SchemaEnv({schema, root, baseId})
279 return getJsonPointer.call(this, p, schOrRef)
282 const PREVENT_SCOPE_CHANGE = new Set([
290 function getJsonPointer(
292 parsedRef: URI.URIComponents,
293 {baseId, schema, root}: SchemaEnv
294 ): SchemaEnv | undefined {
295 if (parsedRef.fragment?.[0] !== "/") return
296 for (const part of parsedRef.fragment.slice(1).split("/")) {
297 if (typeof schema == "boolean") return
298 schema = schema[unescapeFragment(part)]
299 if (schema === undefined) return
300 // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
301 if (!PREVENT_SCOPE_CHANGE.has(part) && typeof schema == "object" && schema.$id) {
302 baseId = resolveUrl(baseId, schema.$id)
305 let env: SchemaEnv | undefined
306 if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
307 const $ref = resolveUrl(baseId, schema.$ref)
308 env = resolveSchema.call(this, root, $ref)
310 // even though resolution failed we need to return SchemaEnv to throw exception
311 // so that compileAsync loads missing schema.
312 env = env || new SchemaEnv({schema, root, baseId})
313 if (env.schema !== env.root.schema) return env