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 | }
|
---|