UNPKG

12.1 kBPlain TextView Raw
1import type {
2 AnySchema,
3 AnySchemaObject,
4 AnyValidateFunction,
5 AsyncValidateFunction,
6 EvaluatedProperties,
7 EvaluatedItems,
8} from "../types"
9import type Ajv from "../core"
10import type {InstanceOptions} from "../core"
11import {CodeGen, _, nil, stringify, Name, Code, ValueScopeName} from "./codegen"
12import ValidationError from "../runtime/validation_error"
13import N from "./names"
14import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve"
15import {schemaHasRulesButRef, unescapeFragment} from "./util"
16import {validateFunctionCode} from "./validate"
17import * as URI from "uri-js"
18import {JSONType} from "./rules"
19
20export type SchemaRefs = {
21 [Ref in string]?: SchemaEnv | AnySchema
22}
23
24export 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
61export interface SchemaObjCxt extends SchemaCxt {
62 readonly schema: AnySchemaObject
63}
64interface 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
74export 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
111export 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
205export 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
226function 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
232export 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
238function 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
244function 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
255export 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
288const PREVENT_SCOPE_CHANGE = new Set([
289 "properties",
290 "patternProperties",
291 "enum",
292 "dependencies",
293 "definitions",
294])
295
296function 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 // TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
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 // even though resolution failed we need to return SchemaEnv to throw exception
319 // so that compileAsync loads missing schema.
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}