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
|
27 | readonly data: Name
|
28 | readonly parentData: Name
|
29 | readonly parentDataProperty: Code | number
|
30 | readonly dataNames: Name[]
|
31 | readonly dataPathArr: (Code | number)[]
|
32 | readonly dataLevel: number
|
33 |
|
34 | dataTypes: JSONType[]
|
35 | definedProperties: Set<string>
|
36 | readonly topSchemaRef: Code
|
37 | readonly validateName: Name
|
38 | evaluated?: Name
|
39 | readonly ValidationError?: Name
|
40 | readonly schema: AnySchema
|
41 | readonly schemaEnv: SchemaEnv
|
42 | readonly rootId: string
|
43 | baseId: string
|
44 | readonly schemaPath: Code
|
45 | readonly errSchemaPath: string
|
46 | readonly errorPath: Code
|
47 | readonly propertyName?: Name
|
48 | readonly compositeRule?: boolean
|
49 |
|
50 |
|
51 |
|
52 | props?: EvaluatedProperties | Name
|
53 | items?: EvaluatedItems | Name
|
54 | jtdDiscriminator?: string
|
55 | jtdMetadata?: boolean
|
56 | readonly createErrors?: boolean
|
57 | readonly opts: InstanceOptions
|
58 | readonly self: Ajv
|
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
|
79 | schemaPath?: string
|
80 | localRefs?: LocalRefs
|
81 | readonly meta?: boolean
|
82 | readonly $async?: boolean
|
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 |
|
108 |
|
109 |
|
110 |
|
111 | export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
|
112 |
|
113 | const _sch = getCompilingSchema.call(this, sch)
|
114 | if (_sch) return _sch
|
115 | const rootId = getFullPath(sch.root.baseId)
|
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],
|
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 |
|
166 | const validateCode = gen.toString()
|
167 | sourceCode = `${gen.scopeRefs(N.scope)}return ${validateCode}`
|
168 |
|
169 | if (this.opts.code.process) sourceCode = this.opts.code.process(sourceCode, sch)
|
170 |
|
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 |
|
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]
|
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 |
|
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 |
|
243 |
|
244 | function resolve(
|
245 | this: Ajv,
|
246 | root: SchemaEnv,
|
247 | ref: string
|
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 |
|
255 | export function resolveSchema(
|
256 | this: Ajv,
|
257 | root: SchemaEnv,
|
258 | ref: string
|
259 | ): SchemaEnv | undefined {
|
260 | const p = URI.parse(ref)
|
261 | const refPath = _getFullPath(p)
|
262 | let baseId = getFullPath(root.baseId)
|
263 |
|
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 | const partSchema = schema[unescapeFragment(part)]
|
305 | if (partSchema === undefined) return
|
306 | schema = partSchema
|
307 |
|
308 | const schId = typeof schema === "object" && schema[this.opts.schemaId]
|
309 | if (!PREVENT_SCOPE_CHANGE.has(part) && schId) {
|
310 | baseId = resolveUrl(baseId, schId)
|
311 | }
|
312 | }
|
313 | let env: SchemaEnv | undefined
|
314 | if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
|
315 | const $ref = resolveUrl(baseId, schema.$ref)
|
316 | env = resolveSchema.call(this, root, $ref)
|
317 | }
|
318 |
|
319 |
|
320 | const {schemaId} = this.opts
|
321 | env = env || new SchemaEnv({schema, schemaId, root, baseId})
|
322 | if (env.schema !== env.root.schema) return env
|
323 | return undefined
|
324 | }
|