ata-validator 1.49.0 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/aot.d.ts +50 -0
- package/aot.mjs +1 -1
- package/bin/ata.js +35 -1
- package/build.d.ts +1 -1
- package/error-runtime.d.ts +24 -0
- package/error-runtime.js +40 -0
- package/error-runtime.mjs +3 -0
- package/index.browser.mjs +14 -5
- package/index.edge.js +14 -0
- package/index.edge.mjs +13 -0
- package/index.js +8 -1014
- package/lib/aot-build.js +386 -69
- package/lib/aot-impl.js +240 -7
- package/lib/codegen-engine.js +1024 -0
- package/lib/error-code-of.js +22 -0
- package/lib/error-runtime-entry.js +85 -0
- package/lib/error-runtime.js +152 -0
- package/lib/error-sort-rank.js +61 -0
- package/lib/interpreter-core.js +1356 -0
- package/lib/interpreter.js +8 -1260
- package/lib/public-api.js +80 -0
- package/lib/schema-order.js +64 -2
- package/lib/schema-runtime-entry.js +45 -0
- package/lib/schema-strip.js +115 -0
- package/lib/tools.mjs +21 -0
- package/lib/validator-core.js +20 -5
- package/lib/version.js +1 -1
- package/package.json +28 -8
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Interpreted draft 2020-12 engine. This is the correctness fallback for
|
|
4
|
+
// schemas the JS codegen cannot represent when the native engine is absent
|
|
5
|
+
// (browser, edge workers, ATA_NO_NATIVE). It walks the schema directly with
|
|
6
|
+
// full annotation tracking (unevaluatedProperties/Items), $id/$anchor
|
|
7
|
+
// resolution, and $dynamicRef dynamic-scope semantics. Each schema node is
|
|
8
|
+
// resolved once into a fixed-shape Plan (type bitmask, compiled pattern,
|
|
9
|
+
// looked-up format, a flag per keyword group) so the walk itself reads no
|
|
10
|
+
// schema properties; codegen and the native engine remain the fast paths.
|
|
11
|
+
|
|
12
|
+
// Read on the first pattern (see lib/js-compiler.js for why).
|
|
13
|
+
let _safeRe = null;
|
|
14
|
+
const compileSafe = (src) => (_safeRe || (_safeRe = require('./safe-regex'))).compileSafe(src);
|
|
15
|
+
|
|
16
|
+
const SCHEMA_KEYWORDS = {
|
|
17
|
+
single: ['additionalProperties', 'contains', 'propertyNames', 'if', 'then', 'else', 'not', 'items', 'unevaluatedItems', 'unevaluatedProperties'],
|
|
18
|
+
maps: ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas'],
|
|
19
|
+
lists: ['allOf', 'anyOf', 'oneOf', 'prefixItems'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const FALLBACK_BASE = 'ata://root';
|
|
23
|
+
|
|
24
|
+
function resolveUri(base, ref) {
|
|
25
|
+
try {
|
|
26
|
+
return new URL(ref, base || FALLBACK_BASE).href;
|
|
27
|
+
} catch {
|
|
28
|
+
return ref;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitFragment(uri) {
|
|
33
|
+
const hash = uri.indexOf('#');
|
|
34
|
+
if (hash < 0) return [uri, ''];
|
|
35
|
+
return [uri.slice(0, hash), decodeURIComponent(uri.slice(hash + 1))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Loaded on the first collapsed anyOf/oneOf error, not with the engine.
|
|
39
|
+
let _collapse = null;
|
|
40
|
+
const collapseBranches = (args) => (_collapse || (_collapse = require('./branch-collapse').collapseBranches))(args);
|
|
41
|
+
|
|
42
|
+
// ---------- resource index ----------
|
|
43
|
+
|
|
44
|
+
// `flat`: the caller knows the schema holds no embedded resource and no
|
|
45
|
+
// anchor (the shared error runtime is told so by the build), so only the root
|
|
46
|
+
// is registered and the walk over every node is skipped. Every node then
|
|
47
|
+
// resolves against the root's base, which is what the walk would record.
|
|
48
|
+
function indexSchemas(rootSchema, schemaMap, flat) {
|
|
49
|
+
const state = {
|
|
50
|
+
resources: new Map(), // baseUri -> schema node of the resource root
|
|
51
|
+
anchors: new Map(), // baseUri -> Map(anchorName -> node)
|
|
52
|
+
dynamicAnchors: new Map(), // baseUri -> Map(anchorName -> node)
|
|
53
|
+
nodeBase: new Map(), // schema node -> baseUri of its enclosing resource
|
|
54
|
+
rootBase: FALLBACK_BASE,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const rootBase =
|
|
58
|
+
typeof rootSchema === 'object' && rootSchema !== null && typeof rootSchema.$id === 'string'
|
|
59
|
+
? resolveUri(FALLBACK_BASE, splitFragment(rootSchema.$id)[0])
|
|
60
|
+
: FALLBACK_BASE;
|
|
61
|
+
state.rootBase = rootBase;
|
|
62
|
+
if (flat === true && !schemaMap && typeof rootSchema === 'object' && rootSchema !== null) {
|
|
63
|
+
state.resources.set(rootBase, rootSchema);
|
|
64
|
+
state.anchors.set(rootBase, new Map());
|
|
65
|
+
state.dynamicAnchors.set(rootBase, new Map());
|
|
66
|
+
state.nodeBase.set(rootSchema, rootBase);
|
|
67
|
+
return state;
|
|
68
|
+
}
|
|
69
|
+
indexResource(rootSchema, rootBase, state);
|
|
70
|
+
|
|
71
|
+
if (schemaMap) {
|
|
72
|
+
for (const [id, schema] of schemaMap) {
|
|
73
|
+
const base = resolveUri(FALLBACK_BASE, splitFragment(id)[0]);
|
|
74
|
+
if (!state.resources.has(base)) indexResource(schema, base, state);
|
|
75
|
+
// Also register under the raw key so non-URI ids stay addressable.
|
|
76
|
+
if (!state.resources.has(id)) state.resources.set(id, schema);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function indexResource(node, baseUri, state) {
|
|
83
|
+
if (typeof node !== 'object' || node === null) return;
|
|
84
|
+
if (!state.resources.has(baseUri)) state.resources.set(baseUri, node);
|
|
85
|
+
if (!state.anchors.has(baseUri)) state.anchors.set(baseUri, new Map());
|
|
86
|
+
if (!state.dynamicAnchors.has(baseUri)) state.dynamicAnchors.set(baseUri, new Map());
|
|
87
|
+
walkSchema(node, baseUri, state, true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkSchema(node, baseUri, state, isResourceRoot) {
|
|
91
|
+
if (typeof node !== 'object' || node === null) return;
|
|
92
|
+
if (state.nodeBase.has(node)) return; // cycle guard
|
|
93
|
+
state.nodeBase.set(node, baseUri);
|
|
94
|
+
|
|
95
|
+
if (!isResourceRoot && typeof node.$id === 'string') {
|
|
96
|
+
// Embedded resource: new base URI, indexed as its own resource.
|
|
97
|
+
const newBase = splitFragment(resolveUri(baseUri, node.$id))[0];
|
|
98
|
+
state.nodeBase.delete(node); // re-register under the new base
|
|
99
|
+
indexResource(node, newBase, state);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof node.$anchor === 'string') state.anchors.get(baseUri).set(node.$anchor, node);
|
|
104
|
+
if (typeof node.$dynamicAnchor === 'string') {
|
|
105
|
+
state.dynamicAnchors.get(baseUri).set(node.$dynamicAnchor, node);
|
|
106
|
+
// A $dynamicAnchor is also addressable as a plain anchor.
|
|
107
|
+
state.anchors.get(baseUri).set(node.$dynamicAnchor, node);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const kw of SCHEMA_KEYWORDS.single) {
|
|
111
|
+
if (node[kw] !== undefined) walkSchema(node[kw], baseUri, state, false);
|
|
112
|
+
}
|
|
113
|
+
for (const kw of SCHEMA_KEYWORDS.maps) {
|
|
114
|
+
const map = node[kw];
|
|
115
|
+
if (map && typeof map === 'object' && !Array.isArray(map)) {
|
|
116
|
+
for (const key of Object.keys(map)) walkSchema(map[key], baseUri, state, false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const kw of SCHEMA_KEYWORDS.lists) {
|
|
120
|
+
const list = node[kw];
|
|
121
|
+
if (Array.isArray(list)) for (const sub of list) walkSchema(sub, baseUri, state, false);
|
|
122
|
+
}
|
|
123
|
+
// propertyDependencies nests one level deeper than the other maps:
|
|
124
|
+
// property name -> property value -> schema.
|
|
125
|
+
const propDeps = node.propertyDependencies;
|
|
126
|
+
if (propDeps && typeof propDeps === 'object' && !Array.isArray(propDeps)) {
|
|
127
|
+
for (const choices of Object.values(propDeps)) {
|
|
128
|
+
if (choices && typeof choices === 'object' && !Array.isArray(choices)) {
|
|
129
|
+
for (const sub of Object.values(choices)) walkSchema(sub, baseUri, state, false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------- ref resolution ----------
|
|
136
|
+
|
|
137
|
+
function walkPointer(root, pointer) {
|
|
138
|
+
if (pointer === '' || pointer === '/') return root;
|
|
139
|
+
const parts = pointer
|
|
140
|
+
.split('/')
|
|
141
|
+
.slice(1)
|
|
142
|
+
.map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
143
|
+
let node = root;
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
if (node === null || typeof node !== 'object') return undefined;
|
|
146
|
+
if (Array.isArray(node)) {
|
|
147
|
+
node = node[Number(part)];
|
|
148
|
+
} else if (part in node) {
|
|
149
|
+
node = node[part];
|
|
150
|
+
} else if (part === 'definitions' && node.$defs) {
|
|
151
|
+
// draft-07 normalization renames definitions to $defs; keep old
|
|
152
|
+
// pointers working (mirrors the 1.0.2 alias fix).
|
|
153
|
+
node = node.$defs;
|
|
154
|
+
} else if (part === '$defs' && node.definitions) {
|
|
155
|
+
node = node.definitions;
|
|
156
|
+
} else if (part === 'items' && Array.isArray(node.prefixItems)) {
|
|
157
|
+
// draft-07 array-form items is normalized to prefixItems; pointers
|
|
158
|
+
// written against the original document still land.
|
|
159
|
+
node = node.prefixItems;
|
|
160
|
+
} else {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return node;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveRef(ref, fromBase, state) {
|
|
168
|
+
const abs = resolveUri(fromBase, ref);
|
|
169
|
+
const [uri, fragment] = splitFragment(abs);
|
|
170
|
+
let resource = state.resources.get(uri);
|
|
171
|
+
let resourceBase = uri;
|
|
172
|
+
if (resource === undefined && (uri === FALLBACK_BASE || uri === '')) {
|
|
173
|
+
resource = state.resources.get(state.rootBase);
|
|
174
|
+
resourceBase = state.rootBase;
|
|
175
|
+
}
|
|
176
|
+
if (resource === undefined) {
|
|
177
|
+
// Raw-id registration (schemaMap keys that are not URIs).
|
|
178
|
+
const [rawUri, rawFragment] = splitFragment(ref);
|
|
179
|
+
if (state.resources.has(rawUri)) {
|
|
180
|
+
resource = state.resources.get(rawUri);
|
|
181
|
+
resourceBase = rawUri;
|
|
182
|
+
if (rawFragment === '') return { node: resource, base: resourceBase };
|
|
183
|
+
if (rawFragment.startsWith('/')) return { node: walkPointer(resource, rawFragment), base: resourceBase };
|
|
184
|
+
const anchored = state.anchors.get(resourceBase);
|
|
185
|
+
return { node: anchored ? anchored.get(rawFragment) : undefined, base: resourceBase };
|
|
186
|
+
}
|
|
187
|
+
return { node: undefined, base: resourceBase };
|
|
188
|
+
}
|
|
189
|
+
if (fragment === '') return { node: resource, base: resourceBase };
|
|
190
|
+
if (fragment.startsWith('/')) {
|
|
191
|
+
const node = walkPointer(resource, fragment);
|
|
192
|
+
// A pointer can land inside an embedded resource; its recorded base wins.
|
|
193
|
+
const base = node !== null && typeof node === 'object' && state.nodeBase.has(node) ? state.nodeBase.get(node) : resourceBase;
|
|
194
|
+
return { node, base };
|
|
195
|
+
}
|
|
196
|
+
const anchored = state.anchors.get(resourceBase);
|
|
197
|
+
return { node: anchored ? anchored.get(fragment) : undefined, base: resourceBase };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------- value helpers ----------
|
|
201
|
+
|
|
202
|
+
// uniqueItems: the functions the code generator emits as _uq and _uqp
|
|
203
|
+
// (lib/unique-items.js), so both engines run the same code.
|
|
204
|
+
const { allDistinct, duplicatePair, _deq } = require('./unique-items');
|
|
205
|
+
|
|
206
|
+
// Whether `data` deep-equals one of the node's enum values (see enumPrims).
|
|
207
|
+
function enumMember(P, data) {
|
|
208
|
+
if (data === null || typeof data !== 'object') return P.enumPrims.has(data);
|
|
209
|
+
const objs = P.enumObjs;
|
|
210
|
+
for (let i = 0; i < objs.length; i++) if (deepEqual(objs[i], data)) return true;
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// JSON equality: the same function the code generator emits as `_deq`, so the
|
|
215
|
+
// engines agree on what equal means (an object's own keys, in any order).
|
|
216
|
+
const deepEqual = _deq;
|
|
217
|
+
|
|
218
|
+
function codePointLength(s) {
|
|
219
|
+
let n = 0;
|
|
220
|
+
for (let i = 0; i < s.length; i++) {
|
|
221
|
+
// Wraparound test: one compare classifies a high surrogate.
|
|
222
|
+
if (((s.charCodeAt(i) - 0xd800) >>> 0) < 0x400 && i + 1 < s.length) i++;
|
|
223
|
+
n++;
|
|
224
|
+
}
|
|
225
|
+
return n;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The code point count of a UTF-16 string sits between ceil(length / 2) and
|
|
229
|
+
// length, so most length bounds are decided without counting anything.
|
|
230
|
+
function cpAtLeast(s, min) {
|
|
231
|
+
if (s.length >= 2 * min) return true;
|
|
232
|
+
if (s.length < min) return false;
|
|
233
|
+
return codePointLength(s) >= min;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function cpAtMost(s, max) {
|
|
237
|
+
if (s.length <= max) return true;
|
|
238
|
+
if (s.length > 2 * max + 1) return false;
|
|
239
|
+
return codePointLength(s) <= max;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function typeMatches(type, d) {
|
|
243
|
+
switch (type) {
|
|
244
|
+
case 'string': return typeof d === 'string';
|
|
245
|
+
case 'number': return Number.isFinite(d);
|
|
246
|
+
case 'integer': return typeof d === 'number' && Number.isInteger(d);
|
|
247
|
+
case 'boolean': return typeof d === 'boolean';
|
|
248
|
+
case 'null': return d === null;
|
|
249
|
+
case 'array': return Array.isArray(d);
|
|
250
|
+
case 'object': return typeof d === 'object' && d !== null && !Array.isArray(d);
|
|
251
|
+
default: return true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function multipleOfOk(d, m) {
|
|
256
|
+
if (m === 0) return false;
|
|
257
|
+
const q = d / m;
|
|
258
|
+
if (Number.isInteger(q)) return true;
|
|
259
|
+
// Float remainder tolerance (0.0075 % 0.0001 style cases)
|
|
260
|
+
return Math.abs(q - Math.round(q)) < 1e-9;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The built-in format checks, built on the first schema that names a format:
|
|
264
|
+
// the engine loads without lib/formats.js, which schemas without `format`
|
|
265
|
+
// never need.
|
|
266
|
+
let _formatChecks = null;
|
|
267
|
+
function buildFormatChecks() {
|
|
268
|
+
const _formats = require('./formats');
|
|
269
|
+
return {
|
|
270
|
+
email: _formats.email,
|
|
271
|
+
date: _formats.date,
|
|
272
|
+
'date-time': _formats.dateTime,
|
|
273
|
+
time: _formats.time,
|
|
274
|
+
duration: _formats.duration,
|
|
275
|
+
uuid: _formats.uuid,
|
|
276
|
+
uri: _formats.uri,
|
|
277
|
+
'uri-reference': _formats.uriReference,
|
|
278
|
+
ipv4: _formats.ipv4,
|
|
279
|
+
ipv6: _formats.ipv6,
|
|
280
|
+
hostname: _formats.hostname,
|
|
281
|
+
regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { return false; } },
|
|
282
|
+
'json-pointer': _formats.jsonPointer,
|
|
283
|
+
'relative-json-pointer': _formats.relativeJsonPointer,
|
|
284
|
+
'uri-template': _formats.uriTemplate,
|
|
285
|
+
iri: _formats.iri,
|
|
286
|
+
'iri-reference': _formats.iriReference,
|
|
287
|
+
'idn-email': _formats.idnEmail,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------- evaluation ----------
|
|
292
|
+
|
|
293
|
+
// Sink for subschema results whose annotations the caller never reads
|
|
294
|
+
// (properties, items, propertyNames and friends). eval() skips the merge when
|
|
295
|
+
// it sees this object, so it is never mutated.
|
|
296
|
+
const DISCARD = { props: null, items: null };
|
|
297
|
+
|
|
298
|
+
// Verdict-only error sink. When eval() sees this identity it neither builds
|
|
299
|
+
// error objects nor formats messages; every push site is guarded on it. The
|
|
300
|
+
// array is frozen so a missed guard throws in tests instead of hiding.
|
|
301
|
+
const NOERRORS = Object.freeze([]);
|
|
302
|
+
|
|
303
|
+
function mergeAnnotations(target, from) {
|
|
304
|
+
if (from.props && from.props.size) {
|
|
305
|
+
if (!target.props) target.props = new Set();
|
|
306
|
+
for (const p of from.props) target.props.add(p);
|
|
307
|
+
}
|
|
308
|
+
if (from.items && from.items.size) {
|
|
309
|
+
if (!target.items) target.items = new Set();
|
|
310
|
+
for (const i of from.items) target.items.add(i);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------- plans ----------
|
|
315
|
+
|
|
316
|
+
// Type bits, one per JSON Schema type. `integer` data also sets the `number`
|
|
317
|
+
// bit so a `number` constraint accepts it.
|
|
318
|
+
const T_STRING = 1;
|
|
319
|
+
const T_NUMBER = 2;
|
|
320
|
+
const T_INTEGER = 4;
|
|
321
|
+
const T_BOOLEAN = 8;
|
|
322
|
+
const T_NULL = 16;
|
|
323
|
+
const T_OBJECT = 32;
|
|
324
|
+
const T_ARRAY = 64;
|
|
325
|
+
const T_ANY = 127;
|
|
326
|
+
|
|
327
|
+
function typeBit(name) {
|
|
328
|
+
switch (name) {
|
|
329
|
+
case 'string': return T_STRING;
|
|
330
|
+
case 'number': return T_NUMBER;
|
|
331
|
+
case 'integer': return T_INTEGER;
|
|
332
|
+
case 'boolean': return T_BOOLEAN;
|
|
333
|
+
case 'null': return T_NULL;
|
|
334
|
+
case 'object': return T_OBJECT;
|
|
335
|
+
case 'array': return T_ARRAY;
|
|
336
|
+
default: return T_ANY; // unknown type names match everything
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function dataBits(d) {
|
|
341
|
+
switch (typeof d) {
|
|
342
|
+
case 'string': return T_STRING;
|
|
343
|
+
case 'number':
|
|
344
|
+
if (!isFinite(d)) return 0;
|
|
345
|
+
return Number.isInteger(d) ? T_NUMBER | T_INTEGER : T_NUMBER;
|
|
346
|
+
case 'boolean': return T_BOOLEAN;
|
|
347
|
+
case 'object':
|
|
348
|
+
if (d === null) return T_NULL;
|
|
349
|
+
return Array.isArray(d) ? T_ARRAY : T_OBJECT;
|
|
350
|
+
default: return 0;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function isObjectMap(v) {
|
|
355
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// A plan is the schema node with every keyword resolved once: the type list
|
|
359
|
+
// folded to a bitmask, the format looked up, patterns compiled, and a flag
|
|
360
|
+
// per section so eval() can skip whole blocks without touching the schema
|
|
361
|
+
// object. Plans have a fixed shape, which keeps eval() monomorphic where the
|
|
362
|
+
// raw schema objects are not.
|
|
363
|
+
class Plan {
|
|
364
|
+
// The plan is registered in the cache before `link` runs, so a schema
|
|
365
|
+
// object that contains itself structurally resolves to the same plan
|
|
366
|
+
// instead of recursing without end.
|
|
367
|
+
constructor(schema) {
|
|
368
|
+
this.schema = schema;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
link(interp) {
|
|
372
|
+
const schema = this.schema;
|
|
373
|
+
// Children are linked as plans (or booleans) up front, so a visit never
|
|
374
|
+
// goes back through the plan cache. Reference targets are resolved at
|
|
375
|
+
// evaluation time and cached per base URI.
|
|
376
|
+
const child = (sub) => interp.node(sub);
|
|
377
|
+
this.refCache = null;
|
|
378
|
+
this.refBase0 = undefined;
|
|
379
|
+
this.refRes0 = null;
|
|
380
|
+
this.dynCache = null;
|
|
381
|
+
this.ref = typeof schema.$ref === 'string' ? schema.$ref : null;
|
|
382
|
+
this.dynamicRef = typeof schema.$dynamicRef === 'string' ? schema.$dynamicRef : null;
|
|
383
|
+
this.tracked = this.ref !== null || this.dynamicRef !== null;
|
|
384
|
+
|
|
385
|
+
this.typeMask = 0;
|
|
386
|
+
this.typeNames = null;
|
|
387
|
+
if (schema.type !== undefined) {
|
|
388
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
389
|
+
let mask = 0;
|
|
390
|
+
for (const t of types) mask |= typeBit(t);
|
|
391
|
+
this.typeMask = mask;
|
|
392
|
+
this.typeNames = types;
|
|
393
|
+
}
|
|
394
|
+
this.hasType = schema.type !== undefined;
|
|
395
|
+
this.enum = schema.enum !== undefined ? schema.enum : null;
|
|
396
|
+
// Membership for enumMember(): the primitive values in a Set, so a long
|
|
397
|
+
// enum answers in one lookup, and the object and array values, which only
|
|
398
|
+
// deepEqual can compare, in a list. Scanning every value with deepEqual
|
|
399
|
+
// was a fifth to a third of what validating SchemaStore's samples cost.
|
|
400
|
+
// NaN stays out of the Set: deepEqual never matches it, and a Set would.
|
|
401
|
+
this.enumPrims = null;
|
|
402
|
+
this.enumObjs = null;
|
|
403
|
+
if (Array.isArray(this.enum)) {
|
|
404
|
+
this.enumPrims = new Set();
|
|
405
|
+
this.enumObjs = [];
|
|
406
|
+
for (const v of this.enum) {
|
|
407
|
+
if (v !== null && typeof v === 'object') this.enumObjs.push(v);
|
|
408
|
+
else if (!(typeof v === 'number' && v !== v)) this.enumPrims.add(v);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.hasConst = schema.const !== undefined;
|
|
412
|
+
this.const = schema.const;
|
|
413
|
+
|
|
414
|
+
this.minimum = typeof schema.minimum === 'number' ? schema.minimum : undefined;
|
|
415
|
+
this.maximum = typeof schema.maximum === 'number' ? schema.maximum : undefined;
|
|
416
|
+
this.exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined;
|
|
417
|
+
this.exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined;
|
|
418
|
+
this.multipleOf = typeof schema.multipleOf === 'number' ? schema.multipleOf : undefined;
|
|
419
|
+
this.hasNumber = this.minimum !== undefined || this.maximum !== undefined || this.exclusiveMinimum !== undefined || this.exclusiveMaximum !== undefined || this.multipleOf !== undefined;
|
|
420
|
+
|
|
421
|
+
this.minLength = schema.minLength;
|
|
422
|
+
this.maxLength = schema.maxLength;
|
|
423
|
+
this.pattern = schema.pattern !== undefined ? interp.pattern(schema.pattern) : null;
|
|
424
|
+
this.patternSource = schema.pattern;
|
|
425
|
+
this.format = schema.format;
|
|
426
|
+
this.formatFn = null;
|
|
427
|
+
if (schema.format !== undefined) {
|
|
428
|
+
const uf = interp.userFormats;
|
|
429
|
+
const fc = (uf && typeof uf[schema.format] === 'function') ? uf[schema.format] : (_formatChecks || (_formatChecks = buildFormatChecks()))[schema.format];
|
|
430
|
+
this.formatFn = fc || null;
|
|
431
|
+
}
|
|
432
|
+
this.hasString = this.minLength !== undefined || this.maxLength !== undefined || this.pattern !== null || this.formatFn !== null;
|
|
433
|
+
|
|
434
|
+
this.minItems = schema.minItems;
|
|
435
|
+
this.maxItems = schema.maxItems;
|
|
436
|
+
this.uniqueItems = schema.uniqueItems === true;
|
|
437
|
+
this.prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems.map(child) : null;
|
|
438
|
+
this.items = schema.items !== undefined ? child(schema.items) : undefined;
|
|
439
|
+
this.contains = schema.contains !== undefined ? child(schema.contains) : undefined;
|
|
440
|
+
this.minContains = schema.minContains;
|
|
441
|
+
this.maxContains = schema.maxContains;
|
|
442
|
+
this.hasArray = this.minItems !== undefined || this.maxItems !== undefined || this.uniqueItems || this.prefixItems !== null || this.items !== undefined || this.contains !== undefined;
|
|
443
|
+
|
|
444
|
+
this.required = Array.isArray(schema.required) ? schema.required : null;
|
|
445
|
+
this.minProperties = schema.minProperties;
|
|
446
|
+
this.maxProperties = schema.maxProperties;
|
|
447
|
+
this.dependentRequired = isObjectMap(schema.dependentRequired) ? Object.entries(schema.dependentRequired) : null;
|
|
448
|
+
this.propertyNames = schema.propertyNames !== undefined ? child(schema.propertyNames) : undefined;
|
|
449
|
+
this.properties = null;
|
|
450
|
+
if (isObjectMap(schema.properties)) {
|
|
451
|
+
this.properties = new Map();
|
|
452
|
+
for (const key of Object.keys(schema.properties)) {
|
|
453
|
+
const ek = escapePointer(key);
|
|
454
|
+
// Escaped once here; the walk only concatenates.
|
|
455
|
+
this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
this.patternProperties = null;
|
|
459
|
+
if (isObjectMap(schema.patternProperties)) {
|
|
460
|
+
this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
|
|
461
|
+
}
|
|
462
|
+
this.additionalProperties = schema.additionalProperties !== undefined ? child(schema.additionalProperties) : undefined;
|
|
463
|
+
this.dependentSchemas = isObjectMap(schema.dependentSchemas) ? Object.entries(schema.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
|
|
464
|
+
this.propertyDependencies = null;
|
|
465
|
+
if (isObjectMap(schema.propertyDependencies)) {
|
|
466
|
+
this.propertyDependencies = Object.entries(schema.propertyDependencies)
|
|
467
|
+
.filter(([, choices]) => isObjectMap(choices))
|
|
468
|
+
.map(([k, choices]) => [k, new Map(Object.keys(choices).map((v) => [v, child(choices[v])]))]);
|
|
469
|
+
}
|
|
470
|
+
this.hasObject = this.required !== null || this.minProperties !== undefined || this.maxProperties !== undefined || this.dependentRequired !== null || this.propertyNames !== undefined || this.properties !== null || this.patternProperties !== null || this.additionalProperties !== undefined || this.dependentSchemas !== null || this.propertyDependencies !== null;
|
|
471
|
+
|
|
472
|
+
this.allOf = Array.isArray(schema.allOf) ? schema.allOf.map(child) : null;
|
|
473
|
+
this.anyOf = Array.isArray(schema.anyOf) ? schema.anyOf.map(child) : null;
|
|
474
|
+
this.oneOf = Array.isArray(schema.oneOf) ? schema.oneOf.map(child) : null;
|
|
475
|
+
this.not = schema.not !== undefined ? child(schema.not) : undefined;
|
|
476
|
+
this.if = schema.if !== undefined ? child(schema.if) : undefined;
|
|
477
|
+
this.then = schema.then !== undefined ? child(schema.then) : undefined;
|
|
478
|
+
this.else = schema.else !== undefined ? child(schema.else) : undefined;
|
|
479
|
+
// Macro keywords: the schema the macro returns is applied in place, the
|
|
480
|
+
// way the reference validator does it, so its errors carry the keyword's
|
|
481
|
+
// path and the keyword adds an error of its own when that schema fails.
|
|
482
|
+
this.macros = null;
|
|
483
|
+
if (interp.keywords !== null) {
|
|
484
|
+
for (const key of Object.keys(schema)) {
|
|
485
|
+
const def = interp.keywords[key];
|
|
486
|
+
if (def === undefined || def.macro === null) continue;
|
|
487
|
+
const expanded = def.macro(schema[key], schema);
|
|
488
|
+
if (expanded === undefined) continue;
|
|
489
|
+
if (this.macros === null) this.macros = [];
|
|
490
|
+
this.macros.push({ keyword: key, node: child(expanded) });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
this.hasApplicators = this.allOf !== null || this.anyOf !== null || this.oneOf !== null || this.not !== undefined || this.if !== undefined || this.macros !== null;
|
|
494
|
+
|
|
495
|
+
this.unevaluatedProperties = schema.unevaluatedProperties !== undefined ? child(schema.unevaluatedProperties) : undefined;
|
|
496
|
+
this.unevaluatedItems = schema.unevaluatedItems !== undefined ? child(schema.unevaluatedItems) : undefined;
|
|
497
|
+
// A `false` here rejects the member outright, and the error belongs to this
|
|
498
|
+
// keyword on this object, not to a boolean schema that happens to sit
|
|
499
|
+
// under it. Recorded at compile time so the check below is one test.
|
|
500
|
+
this.unevaluatedPropertiesFalse = schema.unevaluatedProperties === false;
|
|
501
|
+
this.unevaluatedItemsFalse = schema.unevaluatedItems === false;
|
|
502
|
+
this.hasUnevaluated = this.unevaluatedProperties !== undefined || this.unevaluatedItems !== undefined;
|
|
503
|
+
|
|
504
|
+
// Custom keywords present on this node, in schema order. Each op holds
|
|
505
|
+
// the per-value check, built once here for the compile form, and the
|
|
506
|
+
// type mask it applies to (T_ANY when the definition names no type).
|
|
507
|
+
this.custom = null;
|
|
508
|
+
if (interp.keywords !== null) {
|
|
509
|
+
for (const key of Object.keys(schema)) {
|
|
510
|
+
const def = interp.keywords[key];
|
|
511
|
+
if (def === undefined || def.macro !== null) continue;
|
|
512
|
+
const value = schema[key];
|
|
513
|
+
let mask = T_ANY;
|
|
514
|
+
if (def.types !== null) {
|
|
515
|
+
mask = 0;
|
|
516
|
+
for (const t of def.types) mask |= typeBit(t);
|
|
517
|
+
}
|
|
518
|
+
let fn;
|
|
519
|
+
if (def.compile !== null) {
|
|
520
|
+
fn = def.compile(value, schema);
|
|
521
|
+
if (typeof fn !== 'function') throw new Error(`keyword "${key}": compile must return a function`);
|
|
522
|
+
} else {
|
|
523
|
+
const validate = def.validate;
|
|
524
|
+
fn = (data) => validate(value, data, schema);
|
|
525
|
+
// Errors written by the validate function land on the function
|
|
526
|
+
// itself, which is where the check reads them from below.
|
|
527
|
+
fn.source = validate;
|
|
528
|
+
}
|
|
529
|
+
if (this.custom === null) this.custom = [];
|
|
530
|
+
this.custom.push({ keyword: key, fn, mask });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
this.hasCustom = this.custom !== null;
|
|
534
|
+
|
|
535
|
+
// Resource bookkeeping, read once instead of two Map lookups per visit.
|
|
536
|
+
// Only a resource root changes the base: a node inside a resource is
|
|
537
|
+
// reached with its resource's base already, by descent from the root or
|
|
538
|
+
// through a reference, whose resolution hands over the target's base. The
|
|
539
|
+
// index records a base for every node, and carrying it here made every
|
|
540
|
+
// plan a resource boundary, so no plan was ever a leaf and every value
|
|
541
|
+
// check went through the whole eval() prologue.
|
|
542
|
+
const nodeBase = interp.state.nodeBase.get(schema);
|
|
543
|
+
this.isResourceRoot = nodeBase !== undefined && interp.state.resources.get(nodeBase) === schema;
|
|
544
|
+
this.nodeBase = this.isResourceRoot ? nodeBase : null;
|
|
545
|
+
|
|
546
|
+
// A leaf plan checks only value-level keywords: no references, no
|
|
547
|
+
// subschemas, no annotations, no resource boundary. Those skip the whole
|
|
548
|
+
// eval() prologue (cycle guard, scope, annotation sinks) and run through
|
|
549
|
+
// evalLeaf(). Most schemas are mostly leaves.
|
|
550
|
+
this.leaf = !this.tracked && !this.hasApplicators && !this.hasUnevaluated &&
|
|
551
|
+
this.properties === null && this.patternProperties === null &&
|
|
552
|
+
this.additionalProperties === undefined && this.propertyNames === undefined &&
|
|
553
|
+
this.dependentSchemas === null && this.propertyDependencies === null &&
|
|
554
|
+
this.prefixItems === null && this.items === undefined && this.contains === undefined &&
|
|
555
|
+
this.nodeBase === null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
// Verdict-only twin of evalLeaf: no error objects, no path strings, first
|
|
561
|
+
// failure returns. Kept adjacent so the two cannot drift.
|
|
562
|
+
function evalLeafV(P, data) {
|
|
563
|
+
const bits = dataBits(data);
|
|
564
|
+
if (P.hasType && (bits & P.typeMask) === 0) return false;
|
|
565
|
+
if (P.enum !== null) {
|
|
566
|
+
if (!enumMember(P, data)) return false;
|
|
567
|
+
}
|
|
568
|
+
if (P.hasConst && !deepEqual(P.const, data)) return false;
|
|
569
|
+
if (P.hasNumber && Number.isFinite(data)) {
|
|
570
|
+
if (P.minimum !== undefined && !(data >= P.minimum)) return false;
|
|
571
|
+
if (P.maximum !== undefined && !(data <= P.maximum)) return false;
|
|
572
|
+
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
|
|
573
|
+
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) return false;
|
|
574
|
+
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) return false;
|
|
575
|
+
}
|
|
576
|
+
if (P.hasString && bits === T_STRING) {
|
|
577
|
+
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) return false;
|
|
578
|
+
if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) return false;
|
|
579
|
+
if (P.pattern !== null && !P.pattern.test(data)) return false;
|
|
580
|
+
if (P.formatFn !== null && !P.formatFn(data)) return false;
|
|
581
|
+
}
|
|
582
|
+
if (P.hasArray && bits === T_ARRAY) {
|
|
583
|
+
if (P.minItems !== undefined && data.length < P.minItems) return false;
|
|
584
|
+
if (P.maxItems !== undefined && data.length > P.maxItems) return false;
|
|
585
|
+
if (P.uniqueItems && !allDistinct(data)) return false;
|
|
586
|
+
}
|
|
587
|
+
if (P.hasObject && bits === T_OBJECT) {
|
|
588
|
+
if (P.required !== null) {
|
|
589
|
+
const req = P.required;
|
|
590
|
+
for (let i = 0; i < req.length; i++) {
|
|
591
|
+
if (!Object.hasOwn(data, req[i])) return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (P.minProperties !== undefined && Object.keys(data).length < P.minProperties) return false;
|
|
595
|
+
if (P.maxProperties !== undefined && Object.keys(data).length > P.maxProperties) return false;
|
|
596
|
+
if (P.dependentRequired !== null) {
|
|
597
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
598
|
+
if (Object.hasOwn(data, key)) {
|
|
599
|
+
for (const dep of deps) if (!Object.hasOwn(data, dep)) return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (P.hasCustom && !runCustomV(P, data, bits)) return false;
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Custom keyword checks for one node. The verdict variant stops at the first
|
|
609
|
+
// failing keyword; the collecting variant runs them all and reports each,
|
|
610
|
+
// taking the error objects the keyword's function left on itself when it
|
|
611
|
+
// wrote any (`fn.errors`, the same convention as the default validator's
|
|
612
|
+
// custom keywords) and a fixed message otherwise.
|
|
613
|
+
function runCustomV(P, data, bits) {
|
|
614
|
+
const ops = P.custom;
|
|
615
|
+
for (let i = 0; i < ops.length; i++) {
|
|
616
|
+
const op = ops[i];
|
|
617
|
+
if ((bits & op.mask) === 0) continue;
|
|
618
|
+
const fn = op.fn;
|
|
619
|
+
const ok = fn(data);
|
|
620
|
+
const holder = fn.source || fn;
|
|
621
|
+
if (holder.errors) holder.errors = null;
|
|
622
|
+
if (!ok) return false;
|
|
623
|
+
}
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function runCustom(P, data, bits, errors, instancePath, schemaPath) {
|
|
628
|
+
const ops = P.custom;
|
|
629
|
+
let valid = true;
|
|
630
|
+
for (let i = 0; i < ops.length; i++) {
|
|
631
|
+
const op = ops[i];
|
|
632
|
+
if ((bits & op.mask) === 0) continue;
|
|
633
|
+
const fn = op.fn;
|
|
634
|
+
const ok = fn(data);
|
|
635
|
+
const holder = fn.source || fn;
|
|
636
|
+
const own = holder.errors;
|
|
637
|
+
if (own) holder.errors = null;
|
|
638
|
+
if (ok) continue;
|
|
639
|
+
valid = false;
|
|
640
|
+
if (errors === NOERRORS) return false;
|
|
641
|
+
const kwPath = schemaPath + '/' + op.keyword;
|
|
642
|
+
if (Array.isArray(own) && own.length > 0) {
|
|
643
|
+
for (let j = 0; j < own.length; j++) {
|
|
644
|
+
const e = own[j];
|
|
645
|
+
errors.push(err(op.keyword, e.keyword || op.keyword,
|
|
646
|
+
e.instancePath !== undefined ? instancePath + e.instancePath : instancePath,
|
|
647
|
+
e.schemaPath !== undefined ? e.schemaPath : kwPath,
|
|
648
|
+
e.params !== undefined ? e.params : {},
|
|
649
|
+
e.message !== undefined ? e.message : `must pass "${op.keyword}" keyword validation`));
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
errors.push(err(op.keyword, op.keyword, instancePath, kwPath, { keyword: op.keyword }, `must pass "${op.keyword}" keyword validation`));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return valid;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Value-level checks only; the caller guarantees P.leaf. The bodies mirror
|
|
659
|
+
// the corresponding sections of eval() exactly, minus the prologue.
|
|
660
|
+
function evalLeaf(P, data, errors, instancePath, schemaPath) {
|
|
661
|
+
let valid = true;
|
|
662
|
+
const schema = P.schema;
|
|
663
|
+
const bits = dataBits(data);
|
|
664
|
+
if (P.hasType && (bits & P.typeMask) === 0) {
|
|
665
|
+
if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(', ')}`));
|
|
666
|
+
valid = false;
|
|
667
|
+
}
|
|
668
|
+
if (P.enum !== null) {
|
|
669
|
+
if (!enumMember(P, data)) {
|
|
670
|
+
if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
|
|
671
|
+
valid = false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (P.hasConst && !deepEqual(P.const, data)) {
|
|
675
|
+
if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
|
|
676
|
+
valid = false;
|
|
677
|
+
}
|
|
678
|
+
if (P.hasNumber && Number.isFinite(data)) {
|
|
679
|
+
if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
|
|
680
|
+
if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
|
|
681
|
+
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
|
|
682
|
+
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
|
|
683
|
+
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
|
|
684
|
+
}
|
|
685
|
+
if (P.hasString && bits === T_STRING) {
|
|
686
|
+
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) { errors === NOERRORS || errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
|
|
687
|
+
if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) { errors === NOERRORS || errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
|
|
688
|
+
if (P.pattern !== null && !P.pattern.test(data)) { errors === NOERRORS || errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
|
|
689
|
+
if (P.formatFn !== null && !P.formatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
|
|
690
|
+
}
|
|
691
|
+
if (P.hasArray && bits === T_ARRAY) {
|
|
692
|
+
if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
|
|
693
|
+
if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
|
|
694
|
+
if (P.uniqueItems) {
|
|
695
|
+
const pair = duplicatePair(data);
|
|
696
|
+
if (pair !== null) {
|
|
697
|
+
const i = pair[0], j = pair[1];
|
|
698
|
+
if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
|
|
699
|
+
valid = false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (P.hasObject && bits === T_OBJECT) {
|
|
704
|
+
const keys = Object.keys(data);
|
|
705
|
+
if (P.required !== null) {
|
|
706
|
+
const req = P.required;
|
|
707
|
+
for (let i = 0; i < req.length; i++) {
|
|
708
|
+
const key = req[i];
|
|
709
|
+
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
713
|
+
if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
|
|
714
|
+
if (P.dependentRequired !== null) {
|
|
715
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
716
|
+
if (Object.hasOwn(data, key)) {
|
|
717
|
+
for (const dep of deps) {
|
|
718
|
+
if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (P.hasCustom && !runCustom(P, data, bits, errors, instancePath, schemaPath)) valid = false;
|
|
725
|
+
return valid;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
class Interpreter {
|
|
729
|
+
constructor(rootSchema, options) {
|
|
730
|
+
const opts = options || {};
|
|
731
|
+
this.root = rootSchema;
|
|
732
|
+
this.state = indexSchemas(rootSchema, opts.schemaMap, opts.flat === true);
|
|
733
|
+
this.userFormats = opts.formats || null;
|
|
734
|
+
// Custom keywords, already normalized by lib/keywords.js.
|
|
735
|
+
this.keywords = opts.keywords || null;
|
|
736
|
+
// 2020-12 requires bookending for `$dynamicRef`; v1 removes the
|
|
737
|
+
// requirement. See the `$dynamicRef` branch in eval().
|
|
738
|
+
this.bookending = !opts.v1;
|
|
739
|
+
this.patternCache = new Map();
|
|
740
|
+
// Plans are built on first visit; a schema node is never re-read after.
|
|
741
|
+
this.plans = new Map();
|
|
742
|
+
this.rootNode = this.node(rootSchema);
|
|
743
|
+
// Compiled closure tree: undefined = not attempted, null = out of scope.
|
|
744
|
+
this._fast = undefined;
|
|
745
|
+
this._calls = 0;
|
|
746
|
+
this._tier = typeof opts.tier === 'number' ? opts.tier : TIER;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// The closure-tree compiler covers every schema the interpreter accepts;
|
|
750
|
+
// eval() stays as the reference the compiled tree is diffed against.
|
|
751
|
+
// Compiled lazily so construction stays cheap for schemas that never
|
|
752
|
+
// validate.
|
|
753
|
+
_fastRoot() {
|
|
754
|
+
if (this._fast === undefined) {
|
|
755
|
+
const compile = planCompiler();
|
|
756
|
+
if (compile === null) this._fast = null;
|
|
757
|
+
else try { this._fast = compile(this); } catch { this._fast = null; }
|
|
758
|
+
}
|
|
759
|
+
return this._fast;
|
|
760
|
+
}
|
|
761
|
+
// The closure tree once the schema has been used TIER times, the plain walk
|
|
762
|
+
// before that (null).
|
|
763
|
+
_tiered() {
|
|
764
|
+
if (this._fast !== undefined) return this._fast;
|
|
765
|
+
return ++this._calls > this._tier ? this._fastRoot() : null;
|
|
766
|
+
}
|
|
767
|
+
// The plain walk takes more stack per level of data than the closure tree,
|
|
768
|
+
// which answered every call before calls were tiered: data deep enough to
|
|
769
|
+
// overflow it goes to the closure tree, compiled now, and answers as it did.
|
|
770
|
+
// Without a plan compiler (the shared error runtime) the overflow is
|
|
771
|
+
// rethrown for the caller to handle.
|
|
772
|
+
_deep(e) {
|
|
773
|
+
if (!(e instanceof RangeError) || this._fast !== undefined) throw e;
|
|
774
|
+
const fast = this._fastRoot();
|
|
775
|
+
if (fast === null) throw e;
|
|
776
|
+
return fast;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
pattern(src) {
|
|
780
|
+
let re = this.patternCache.get(src);
|
|
781
|
+
if (!re) {
|
|
782
|
+
if (/\\[pP]\{/.test(src)) {
|
|
783
|
+
// Unicode property escapes need a real RegExp in unicode mode; the
|
|
784
|
+
// linear engine does not understand them.
|
|
785
|
+
try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
|
|
786
|
+
} else {
|
|
787
|
+
try {
|
|
788
|
+
re = compileSafe(src);
|
|
789
|
+
} catch {
|
|
790
|
+
try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
this.patternCache.set(src, re);
|
|
794
|
+
}
|
|
795
|
+
return re;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
plan(schema) {
|
|
799
|
+
let p = this.plans.get(schema);
|
|
800
|
+
if (p === undefined) {
|
|
801
|
+
p = new Plan(schema);
|
|
802
|
+
this.plans.set(schema, p);
|
|
803
|
+
p.link(this);
|
|
804
|
+
}
|
|
805
|
+
return p;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// $ref resolution depends on the base URI in effect, which can differ
|
|
809
|
+
// between visits of the same node; cache per (plan, base). Nearly every
|
|
810
|
+
// plan only ever sees one base, so the last resolution lives in two plain
|
|
811
|
+
// fields and the Map exists only for the rare second base. The cached
|
|
812
|
+
// entry carries the already-planned child, so a visit does no Map lookups.
|
|
813
|
+
resolveRefCached(P, base) {
|
|
814
|
+
if (P.refBase0 === base) return P.refRes0;
|
|
815
|
+
let cache = P.refCache;
|
|
816
|
+
let r = cache !== null ? cache.get(base) : undefined;
|
|
817
|
+
if (r === undefined) {
|
|
818
|
+
const raw = resolveRef(P.ref, base, this.state);
|
|
819
|
+
r = { child: raw.node === undefined ? undefined : this.node(raw.node), base: raw.base };
|
|
820
|
+
if (P.refBase0 === undefined) { P.refBase0 = base; P.refRes0 = r; }
|
|
821
|
+
else { if (cache === null) cache = P.refCache = new Map(); cache.set(base, r); }
|
|
822
|
+
}
|
|
823
|
+
return r;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// The static half of $dynamicRef (initial target and fragment) per base;
|
|
827
|
+
// the dynamic-scope search still runs on every visit.
|
|
828
|
+
resolveDynamicRefCached(P, base) {
|
|
829
|
+
let cache = P.dynCache;
|
|
830
|
+
if (cache === null) cache = P.dynCache = new Map();
|
|
831
|
+
let r = cache.get(base);
|
|
832
|
+
if (r === undefined) {
|
|
833
|
+
const { node, base: refBase } = resolveRef(P.dynamicRef, base, this.state);
|
|
834
|
+
const [, fragment] = splitFragment(resolveUri(base, P.dynamicRef));
|
|
835
|
+
r = { node, base: refBase, fragment, byAnchor: Boolean(fragment) && !fragment.startsWith('/') };
|
|
836
|
+
cache.set(base, r);
|
|
837
|
+
}
|
|
838
|
+
return r;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Maps any schema value to what eval() accepts: a boolean or a Plan.
|
|
842
|
+
// Non-object, non-boolean values validate everything, as before.
|
|
843
|
+
node(schema) {
|
|
844
|
+
if (schema === true || schema === false) return schema;
|
|
845
|
+
if (typeof schema !== 'object' || schema === null) return true;
|
|
846
|
+
return this.plan(schema);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Verdict only: same walk as validate(), no error construction at all.
|
|
850
|
+
isValid(data) {
|
|
851
|
+
const fast = this._fast !== undefined ? this._fast : this._tiered();
|
|
852
|
+
if (fast !== null) return fast.v(data, fast.cyclic ? [] : null);
|
|
853
|
+
const dynScope = [this.state.rootBase];
|
|
854
|
+
try {
|
|
855
|
+
return this.eval(this.rootNode, data, this.state.rootBase, dynScope, NOERRORS, '', '#', [], DISCARD);
|
|
856
|
+
} catch (e) {
|
|
857
|
+
const deep = this._deep(e);
|
|
858
|
+
return deep.v(data, deep.cyclic ? [] : null);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
validate(data) {
|
|
863
|
+
const fast = this._fast !== undefined ? this._fast : this._tiered();
|
|
864
|
+
if (fast !== null) {
|
|
865
|
+
const errors = [];
|
|
866
|
+
const valid = fast.c(data, errors, '', '#', fast.cyclic ? [] : null);
|
|
867
|
+
return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
|
|
868
|
+
}
|
|
869
|
+
let errors = [];
|
|
870
|
+
const dynScope = [this.state.rootBase];
|
|
871
|
+
let valid;
|
|
872
|
+
try {
|
|
873
|
+
// Nothing reads the root's annotations, so they are discarded from the start.
|
|
874
|
+
valid = this.eval(this.rootNode, data, this.state.rootBase, dynScope, errors, '', '#', [], DISCARD);
|
|
875
|
+
} catch (e) {
|
|
876
|
+
const deep = this._deep(e);
|
|
877
|
+
errors = [];
|
|
878
|
+
valid = deep.c(data, errors, '', '#', deep.cyclic ? [] : null);
|
|
879
|
+
}
|
|
880
|
+
return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Evaluates `schema` against `data`. Collected annotations for data are
|
|
884
|
+
// written into `sink` ({props, items}); errors append to `errors`.
|
|
885
|
+
// `stack` breaks infinite $ref recursion on identical (schema, data) pairs.
|
|
886
|
+
eval(P, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
|
|
887
|
+
// Paths are built only while errors are collected; a verdict walk never
|
|
888
|
+
// reads them, and concatenating two per child was most of its cost.
|
|
889
|
+
const E = errors !== NOERRORS;
|
|
890
|
+
if (P === true) return true;
|
|
891
|
+
if (P === false) {
|
|
892
|
+
if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (P.leaf) return evalLeaf(P, data, errors, instancePath, schemaPath);
|
|
896
|
+
const schema = P.schema;
|
|
897
|
+
|
|
898
|
+
// Only a reference can lead evaluation back to a schema it is already
|
|
899
|
+
// inside of, so the cycle guard is maintained on reference nodes alone.
|
|
900
|
+
// The frame is pushed in place and popped on the way out; the array is
|
|
901
|
+
// created per validate() call, so a throw leaves nothing to clean up.
|
|
902
|
+
const tracked = P.tracked;
|
|
903
|
+
if (tracked) {
|
|
904
|
+
for (let i = stack.length - 2; i >= 0; i -= 2) {
|
|
905
|
+
if (stack[i] === schema && stack[i + 1] === data) return true; // fixed point
|
|
906
|
+
}
|
|
907
|
+
stack.push(schema, data);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (P.nodeBase !== null && P.nodeBase !== base) base = P.nodeBase;
|
|
911
|
+
// Entering a schema resource extends the dynamic scope, whether evaluation
|
|
912
|
+
// lands on the resource's root or on a subschema inside it: a reference
|
|
913
|
+
// to first#/$defs/stuff still passes through resource `first`. The scope
|
|
914
|
+
// is pushed in place and popped on the way out; evaluation is depth-first
|
|
915
|
+
// and nothing retains the array.
|
|
916
|
+
let scopePushed = false;
|
|
917
|
+
if (dynScope[dynScope.length - 1] !== base) {
|
|
918
|
+
dynScope.push(base);
|
|
919
|
+
scopePushed = true;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
let valid = true;
|
|
923
|
+
// Annotations only matter if this node or an ancestor reads them. When
|
|
924
|
+
// nobody does, in-place children get the discard sink too and `local`
|
|
925
|
+
// stays unallocated.
|
|
926
|
+
const collect = sink !== DISCARD || P.hasUnevaluated;
|
|
927
|
+
const local = collect ? { props: null, items: null } : DISCARD;
|
|
928
|
+
|
|
929
|
+
// $ref / $dynamicRef: in-place applicators, annotations flow through
|
|
930
|
+
if (P.ref !== null) {
|
|
931
|
+
const { child, base: refBase } = this.resolveRefCached(P, base);
|
|
932
|
+
if (child === undefined) {
|
|
933
|
+
if (errors !== NOERRORS) errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: P.ref }, `cannot resolve $ref ${P.ref}`));
|
|
934
|
+
valid = false;
|
|
935
|
+
} else {
|
|
936
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
937
|
+
if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath, stack, sub)) valid = false;
|
|
938
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (P.dynamicRef !== null) {
|
|
943
|
+
const ref = P.dynamicRef;
|
|
944
|
+
const initial = this.resolveDynamicRefCached(P, base);
|
|
945
|
+
let node = initial.node;
|
|
946
|
+
let refBase = initial.base;
|
|
947
|
+
const fragment = initial.fragment;
|
|
948
|
+
if (initial.byAnchor) {
|
|
949
|
+
// Under 2020-12 the initial target must itself carry the matching
|
|
950
|
+
// $dynamicAnchor before the dynamic scope is consulted (bookending);
|
|
951
|
+
// otherwise the keyword behaves like $ref. v1 removes that
|
|
952
|
+
// requirement, so the scope is searched whether or not the initial
|
|
953
|
+
// target carries the anchor, and also when the reference does not
|
|
954
|
+
// resolve on its own, which is the shape the suite uses: a
|
|
955
|
+
// `$dynamicRef` in a resource that declares no anchor of that name.
|
|
956
|
+
const initialDyn = this.state.dynamicAnchors.get(refBase);
|
|
957
|
+
const bookended = node !== undefined && initialDyn && initialDyn.get(fragment) === node;
|
|
958
|
+
if (bookended || !this.bookending) {
|
|
959
|
+
// Outermost scope first: the first matching $dynamicAnchor
|
|
960
|
+
// encountered while evaluating wins.
|
|
961
|
+
for (const scopeBase of dynScope) {
|
|
962
|
+
const dyn = this.state.dynamicAnchors.get(scopeBase);
|
|
963
|
+
if (dyn && dyn.has(fragment)) {
|
|
964
|
+
node = dyn.get(fragment);
|
|
965
|
+
refBase = scopeBase;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (node === undefined) {
|
|
972
|
+
if (errors !== NOERRORS) errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
|
|
973
|
+
valid = false;
|
|
974
|
+
} else {
|
|
975
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
976
|
+
if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath, stack, sub)) valid = false;
|
|
977
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const bits = dataBits(data);
|
|
982
|
+
|
|
983
|
+
// type / enum / const
|
|
984
|
+
if (P.hasType && (bits & P.typeMask) === 0) {
|
|
985
|
+
if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(', ')}`));
|
|
986
|
+
valid = false;
|
|
987
|
+
}
|
|
988
|
+
if (P.enum !== null) {
|
|
989
|
+
if (!enumMember(P, data)) {
|
|
990
|
+
if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
|
|
991
|
+
valid = false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (P.hasConst && !deepEqual(P.const, data)) {
|
|
995
|
+
if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
|
|
996
|
+
valid = false;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// numbers
|
|
1000
|
+
if (P.hasNumber && Number.isFinite(data)) {
|
|
1001
|
+
if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
|
|
1002
|
+
if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
|
|
1003
|
+
if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
|
|
1004
|
+
if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) { errors === NOERRORS || errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: P.exclusiveMaximum }, `must be < ${P.exclusiveMaximum}`)); valid = false; }
|
|
1005
|
+
if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) { errors === NOERRORS || errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: P.multipleOf }, `must be multiple of ${P.multipleOf}`)); valid = false; }
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// strings
|
|
1009
|
+
if (P.hasString && bits === T_STRING) {
|
|
1010
|
+
if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) { errors === NOERRORS || errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: P.minLength }, `must NOT have fewer than ${P.minLength} characters`)); valid = false; }
|
|
1011
|
+
if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) { errors === NOERRORS || errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: P.maxLength }, `must NOT have more than ${P.maxLength} characters`)); valid = false; }
|
|
1012
|
+
if (P.pattern !== null && !P.pattern.test(data)) { errors === NOERRORS || errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: P.patternSource }, `must match pattern "${P.patternSource}"`)); valid = false; }
|
|
1013
|
+
if (P.formatFn !== null && !P.formatFn(data)) { errors === NOERRORS || errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: P.format }, `must match format "${P.format}"`)); valid = false; }
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Custom keywords, where the plan compiler runs them: with the other
|
|
1017
|
+
// value-level checks (evalLeaf), before any subschema. Only evalLeaf ran
|
|
1018
|
+
// them, so on a node that was not a leaf this walk skipped them and
|
|
1019
|
+
// accepted what they reject; the plan compiler, which answered every call
|
|
1020
|
+
// until calls were tiered, hid it.
|
|
1021
|
+
if (P.hasCustom && !runCustom(P, data, bits, errors, instancePath, schemaPath)) valid = false;
|
|
1022
|
+
|
|
1023
|
+
// arrays
|
|
1024
|
+
if (P.hasArray && bits === T_ARRAY) {
|
|
1025
|
+
if (P.minItems !== undefined && data.length < P.minItems) { errors === NOERRORS || errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: P.minItems }, `must NOT have fewer than ${P.minItems} items`)); valid = false; }
|
|
1026
|
+
if (P.maxItems !== undefined && data.length > P.maxItems) { errors === NOERRORS || errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: P.maxItems }, `must NOT have more than ${P.maxItems} items`)); valid = false; }
|
|
1027
|
+
if (P.uniqueItems) {
|
|
1028
|
+
const pair = duplicatePair(data);
|
|
1029
|
+
if (pair !== null) {
|
|
1030
|
+
const i = pair[0], j = pair[1];
|
|
1031
|
+
if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
|
|
1032
|
+
valid = false;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const prefix = P.prefixItems;
|
|
1036
|
+
if (prefix !== null) {
|
|
1037
|
+
const n = Math.min(prefix.length, data.length);
|
|
1038
|
+
for (let i = 0; i < n; i++) {
|
|
1039
|
+
if (!this.eval(prefix[i], data[i], base, dynScope, errors, E ? instancePath + '/' + i : '', E ? schemaPath + '/prefixItems/' + i : '', stack, DISCARD)) valid = false;
|
|
1040
|
+
if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (P.items !== undefined) {
|
|
1044
|
+
const start = prefix !== null ? prefix.length : 0;
|
|
1045
|
+
for (let i = start; i < data.length; i++) {
|
|
1046
|
+
if (!this.eval(P.items, data[i], base, dynScope, errors, E ? instancePath + '/' + i : '', E ? schemaPath + '/items' : '', stack, DISCARD)) valid = false;
|
|
1047
|
+
if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (P.contains !== undefined) {
|
|
1051
|
+
const matched = [];
|
|
1052
|
+
for (let i = 0; i < data.length; i++) {
|
|
1053
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1054
|
+
if (this.eval(P.contains, data[i], base, dynScope, scratch, E ? instancePath + '/' + i : '', E ? schemaPath + '/contains' : '', stack, DISCARD)) matched.push(i);
|
|
1055
|
+
}
|
|
1056
|
+
const minC = P.minContains !== undefined ? P.minContains : 1;
|
|
1057
|
+
if (matched.length < minC) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
|
|
1058
|
+
if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC, maxContains: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
|
|
1059
|
+
if (collect && matched.length) {
|
|
1060
|
+
if (!local.items) local.items = new Set();
|
|
1061
|
+
for (const i of matched) local.items.add(i);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// objects
|
|
1067
|
+
if (P.hasObject && bits === T_OBJECT) {
|
|
1068
|
+
const keys = Object.keys(data);
|
|
1069
|
+
if (P.required !== null) {
|
|
1070
|
+
const req = P.required;
|
|
1071
|
+
for (let i = 0; i < req.length; i++) {
|
|
1072
|
+
const key = req[i];
|
|
1073
|
+
if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (P.minProperties !== undefined && keys.length < P.minProperties) { errors === NOERRORS || errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: P.minProperties }, `must NOT have fewer than ${P.minProperties} properties`)); valid = false; }
|
|
1077
|
+
if (P.maxProperties !== undefined && keys.length > P.maxProperties) { errors === NOERRORS || errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: P.maxProperties }, `must NOT have more than ${P.maxProperties} properties`)); valid = false; }
|
|
1078
|
+
if (P.dependentRequired !== null) {
|
|
1079
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
1080
|
+
if (Object.hasOwn(data, key)) {
|
|
1081
|
+
for (const dep of deps) {
|
|
1082
|
+
if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (P.propertyNames !== undefined) {
|
|
1088
|
+
for (const key of keys) {
|
|
1089
|
+
if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath, E ? schemaPath + '/propertyNames' : '', stack, DISCARD)) valid = false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const props = P.properties;
|
|
1093
|
+
const patterns = P.patternProperties;
|
|
1094
|
+
for (let k = 0; k < keys.length; k++) {
|
|
1095
|
+
const key = keys[k];
|
|
1096
|
+
let evaluated = false;
|
|
1097
|
+
const prop = props !== null ? props.get(key) : undefined;
|
|
1098
|
+
if (prop !== undefined) {
|
|
1099
|
+
if (!this.eval(prop.node, data[key], base, dynScope, errors, E ? instancePath + prop.seg : '', E ? schemaPath + prop.schemaSeg : '', stack, DISCARD)) valid = false;
|
|
1100
|
+
evaluated = true;
|
|
1101
|
+
}
|
|
1102
|
+
if (patterns !== null) {
|
|
1103
|
+
for (let pi = 0; pi < patterns.length; pi++) {
|
|
1104
|
+
const pp = patterns[pi];
|
|
1105
|
+
if (pp.re.test(key)) {
|
|
1106
|
+
if (!this.eval(pp.node, data[key], base, dynScope, errors, E ? instancePath + '/' + escapePointer(key) : '', E ? schemaPath + '/patternProperties/' + escapePointer(pp.src) : '', stack, DISCARD)) valid = false;
|
|
1107
|
+
evaluated = true;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (!evaluated && P.additionalProperties !== undefined) {
|
|
1112
|
+
// `additionalProperties: false` keeps its own keyword and names the
|
|
1113
|
+
// property it turned away. Running it as a plain false subschema
|
|
1114
|
+
// reported `not` at the key's path, which drops both.
|
|
1115
|
+
if (P.additionalProperties === false) {
|
|
1116
|
+
valid = false;
|
|
1117
|
+
if (errors !== NOERRORS) errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
|
|
1118
|
+
} else if (!this.eval(P.additionalProperties, data[key], base, dynScope, errors, E ? instancePath + '/' + escapePointer(key) : '', E ? schemaPath + '/additionalProperties' : '', stack, DISCARD)) valid = false;
|
|
1119
|
+
evaluated = true;
|
|
1120
|
+
}
|
|
1121
|
+
if (evaluated && collect) {
|
|
1122
|
+
if (!local.props) local.props = new Set();
|
|
1123
|
+
local.props.add(key);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (P.dependentSchemas !== null) {
|
|
1127
|
+
for (const [key, dep] of P.dependentSchemas) {
|
|
1128
|
+
if (Object.hasOwn(data, key)) {
|
|
1129
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1130
|
+
if (!this.eval(dep, data, base, dynScope, errors, instancePath, E ? schemaPath + '/dependentSchemas/' + escapePointer(key) : '', stack, sub)) valid = false;
|
|
1131
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// propertyDependencies: like dependentSchemas but keyed on the property's
|
|
1136
|
+
// value rather than its presence. Only string values can match, since the
|
|
1137
|
+
// inner keys are object keys. Equivalent to
|
|
1138
|
+
// `if: { properties: { p: { const: v } }, required: [p] }, then: <schema>`.
|
|
1139
|
+
if (P.propertyDependencies !== null) {
|
|
1140
|
+
for (const [key, choices] of P.propertyDependencies) {
|
|
1141
|
+
if (!Object.hasOwn(data, key)) continue;
|
|
1142
|
+
const value = data[key];
|
|
1143
|
+
if (typeof value !== 'string') continue;
|
|
1144
|
+
const choice = choices.get(value);
|
|
1145
|
+
if (choice === undefined) continue;
|
|
1146
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1147
|
+
const branchPath = schemaPath + '/propertyDependencies/' + escapePointer(key) + '/' + escapePointer(value);
|
|
1148
|
+
if (!this.eval(choice, data, base, dynScope, errors, instancePath, branchPath, stack, sub)) valid = false;
|
|
1149
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// in-place applicators
|
|
1155
|
+
if (P.hasApplicators) {
|
|
1156
|
+
if (P.allOf !== null) {
|
|
1157
|
+
for (let i = 0; i < P.allOf.length; i++) {
|
|
1158
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1159
|
+
if (!this.eval(P.allOf[i], data, base, dynScope, errors, instancePath, E ? schemaPath + '/allOf/' + i : '', stack, sub)) valid = false;
|
|
1160
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (P.macros !== null) {
|
|
1164
|
+
for (let i = 0; i < P.macros.length; i++) {
|
|
1165
|
+
const m = P.macros[i];
|
|
1166
|
+
const sp = schemaPath + '/' + m.keyword;
|
|
1167
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1168
|
+
if (!this.eval(m.node, data, base, dynScope, errors, instancePath, sp, stack, sub)) {
|
|
1169
|
+
valid = false;
|
|
1170
|
+
if (errors !== NOERRORS) errors.push(err(m.keyword, m.keyword, instancePath, sp, { keyword: m.keyword }, `must pass "${m.keyword}" keyword validation`));
|
|
1171
|
+
} else if (collect) mergeAnnotations(local, sub);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (P.anyOf !== null) {
|
|
1175
|
+
let any = false;
|
|
1176
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1177
|
+
// Per-branch errors, taken as a slice of the shared scratch rather than
|
|
1178
|
+
// an array each, so a branch that passes costs nothing extra.
|
|
1179
|
+
const branches = errors === NOERRORS ? null : [];
|
|
1180
|
+
for (let i = 0; i < P.anyOf.length; i++) {
|
|
1181
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1182
|
+
const mark = branches === null ? 0 : scratch.length;
|
|
1183
|
+
const ok = this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, E ? schemaPath + '/anyOf/' + i : '', stack, sub);
|
|
1184
|
+
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.anyOf[i]) });
|
|
1185
|
+
if (ok) {
|
|
1186
|
+
any = true;
|
|
1187
|
+
if (collect) mergeAnnotations(local, sub);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (!any) {
|
|
1191
|
+
if (branches !== null) {
|
|
1192
|
+
const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
|
|
1193
|
+
if (collapsed !== null) errors.push(collapsed);
|
|
1194
|
+
}
|
|
1195
|
+
valid = false;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (P.oneOf !== null) {
|
|
1199
|
+
let count = 0;
|
|
1200
|
+
let winner = null;
|
|
1201
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1202
|
+
// Per-branch results, so a failing oneOf collapses to the branch that
|
|
1203
|
+
// came closest instead of every branch's complaints at once.
|
|
1204
|
+
const branches = errors === NOERRORS ? null : [];
|
|
1205
|
+
for (let i = 0; i < P.oneOf.length; i++) {
|
|
1206
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1207
|
+
const mark = branches === null ? 0 : scratch.length;
|
|
1208
|
+
const ok = this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, E ? schemaPath + '/oneOf/' + i : '', stack, sub);
|
|
1209
|
+
if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: branchTitle(P.oneOf[i]) });
|
|
1210
|
+
if (ok) {
|
|
1211
|
+
count++;
|
|
1212
|
+
winner = sub;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (count === 1) { if (collect) mergeAnnotations(local, winner); }
|
|
1216
|
+
else {
|
|
1217
|
+
if (branches !== null) {
|
|
1218
|
+
const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
|
|
1219
|
+
if (collapsed !== null) errors.push(collapsed);
|
|
1220
|
+
}
|
|
1221
|
+
valid = false;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (P.not !== undefined) {
|
|
1225
|
+
const scratch = errors === NOERRORS ? NOERRORS : [];
|
|
1226
|
+
if (this.eval(P.not, data, base, dynScope, scratch, instancePath, E ? schemaPath + '/not' : '', stack, DISCARD)) {
|
|
1227
|
+
if (errors !== NOERRORS) errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
|
|
1228
|
+
valid = false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (P.if !== undefined) {
|
|
1232
|
+
const ifSub = collect ? { props: null, items: null } : DISCARD;
|
|
1233
|
+
const ifScratch = errors === NOERRORS ? NOERRORS : [];
|
|
1234
|
+
const ifOk = this.eval(P.if, data, base, dynScope, ifScratch, instancePath, E ? schemaPath + '/if' : '', stack, ifSub);
|
|
1235
|
+
if (ifOk) {
|
|
1236
|
+
if (collect) mergeAnnotations(local, ifSub);
|
|
1237
|
+
if (P.then !== undefined) {
|
|
1238
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1239
|
+
if (!this.eval(P.then, data, base, dynScope, errors, instancePath, E ? schemaPath + '/then' : '', stack, sub)) valid = false;
|
|
1240
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
1241
|
+
}
|
|
1242
|
+
} else if (P.else !== undefined) {
|
|
1243
|
+
const sub = collect ? { props: null, items: null } : DISCARD;
|
|
1244
|
+
if (!this.eval(P.else, data, base, dynScope, errors, instancePath, E ? schemaPath + '/else' : '', stack, sub)) valid = false;
|
|
1245
|
+
else if (collect) mergeAnnotations(local, sub);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// unevaluated*: run last, against annotations from everything above
|
|
1251
|
+
if (P.hasUnevaluated) {
|
|
1252
|
+
if (P.unevaluatedProperties !== undefined && bits === T_OBJECT) {
|
|
1253
|
+
for (const key of Object.keys(data)) {
|
|
1254
|
+
if (local.props && local.props.has(key)) continue;
|
|
1255
|
+
if (P.unevaluatedPropertiesFalse) {
|
|
1256
|
+
// The name is what is wrong, so the error names it and sits on the
|
|
1257
|
+
// object holding it. Descending instead reported a false boolean
|
|
1258
|
+
// schema at the member's own path, which says nothing about which
|
|
1259
|
+
// key was unexpected and left `params` empty.
|
|
1260
|
+
valid = false;
|
|
1261
|
+
if (errors !== NOERRORS) errors.push(err('unevaluatedProperties', 'unevaluatedProperties', instancePath, schemaPath + '/unevaluatedProperties', { unevaluatedProperty: key }, 'must NOT have unevaluated properties'));
|
|
1262
|
+
} else if (!this.eval(P.unevaluatedProperties, data[key], base, dynScope, errors, E ? instancePath + '/' + escapePointer(key) : '', E ? schemaPath + '/unevaluatedProperties' : '', stack, DISCARD)) valid = false;
|
|
1263
|
+
if (!local.props) local.props = new Set();
|
|
1264
|
+
local.props.add(key);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (P.unevaluatedItems !== undefined && bits === T_ARRAY) {
|
|
1268
|
+
for (let i = 0; i < data.length; i++) {
|
|
1269
|
+
if (local.items && local.items.has(i)) continue;
|
|
1270
|
+
if (P.unevaluatedItemsFalse) {
|
|
1271
|
+
// One error for the array, not one per trailing element: what the
|
|
1272
|
+
// array got wrong is its length past the last evaluated position.
|
|
1273
|
+
valid = false;
|
|
1274
|
+
if (errors !== NOERRORS) errors.push(err('unevaluatedItems', 'unevaluatedItems', instancePath, schemaPath + '/unevaluatedItems', { limit: i }, 'must NOT have more than ' + i + ' items'));
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
if (!this.eval(P.unevaluatedItems, data[i], base, dynScope, errors, E ? instancePath + '/' + i : '', E ? schemaPath + '/unevaluatedItems' : '', stack, DISCARD)) valid = false;
|
|
1278
|
+
if (!local.items) local.items = new Set();
|
|
1279
|
+
local.items.add(i);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (scopePushed) dynScope.pop();
|
|
1285
|
+
if (tracked) stack.length -= 2;
|
|
1286
|
+
if (valid && collect && sink !== DISCARD) mergeAnnotations(sink, local);
|
|
1287
|
+
return valid;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// One branch title, for the collapsed anyOf/oneOf error. The compiled node
|
|
1292
|
+
// keeps the schema it came from, which is where a `title` lives.
|
|
1293
|
+
function branchTitle (node) {
|
|
1294
|
+
return node && node.schema && typeof node.schema.title === 'string' ? node.schema.title : '';
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function escapePointer(s) {
|
|
1298
|
+
if (typeof s !== 'string') s = String(s);
|
|
1299
|
+
// Keys almost never contain `~` or `/`; skip the two regex passes when so.
|
|
1300
|
+
for (let i = 0; i < s.length; i++) {
|
|
1301
|
+
const c = s.charCodeAt(i);
|
|
1302
|
+
if (c === 0x7e || c === 0x2f) return s.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
1303
|
+
}
|
|
1304
|
+
return s;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function err(code, keyword, instancePath, schemaPath, params, message) {
|
|
1308
|
+
return { keyword, instancePath, schemaPath, params, message };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// The plan compiler (closure trees for the verdict and for errors) is wired in
|
|
1312
|
+
// by lib/interpreter.js. This file works without it, walking the plans
|
|
1313
|
+
// directly with the same answers, so the error runtime compiled modules share
|
|
1314
|
+
// (lib/error-runtime-entry.js) can carry the interpreter without it.
|
|
1315
|
+
//
|
|
1316
|
+
// It is loaded and run only for a schema that has been used often (TIER
|
|
1317
|
+
// calls): building the closure tree for a whole schema costs about as much as
|
|
1318
|
+
// a few thousand plain walks, measured on SchemaStore schemas, so a page or a
|
|
1319
|
+
// short-lived worker that checks a handful of documents never pays for it, or
|
|
1320
|
+
// for parsing its source.
|
|
1321
|
+
const TIER = 1000;
|
|
1322
|
+
let loadPlanCompiler = null;
|
|
1323
|
+
let compileInterpreter = null;
|
|
1324
|
+
function setPlanCompiler(load) {
|
|
1325
|
+
loadPlanCompiler = load;
|
|
1326
|
+
compileInterpreter = null;
|
|
1327
|
+
}
|
|
1328
|
+
function planCompiler() {
|
|
1329
|
+
if (compileInterpreter === null && loadPlanCompiler !== null) {
|
|
1330
|
+
compileInterpreter = loadPlanCompiler()({
|
|
1331
|
+
Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
1332
|
+
deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
|
|
1333
|
+
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
|
|
1334
|
+
}).compileInterpreter;
|
|
1335
|
+
}
|
|
1336
|
+
return compileInterpreter;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function createInterpreter(schema, options) {
|
|
1340
|
+
return new Interpreter(schema, options);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
module.exports = { createInterpreter };
|
|
1344
|
+
Object.defineProperty(module.exports, '_setPlanCompiler', { value: setPlanCompiler, enumerable: false });
|
|
1345
|
+
// The resolver, for the code generator: it resolves references at compile
|
|
1346
|
+
// time the way this engine does at run time (normalizeRefs in js-compiler).
|
|
1347
|
+
Object.defineProperty(module.exports, '_refInternals', {
|
|
1348
|
+
value: { indexSchemas, resolveRef, splitFragment, resolveUri, FALLBACK_BASE },
|
|
1349
|
+
enumerable: false,
|
|
1350
|
+
});
|
|
1351
|
+
// For lib/plan-source.js, which emits source from the same plans and must call
|
|
1352
|
+
// the same helpers so the two cannot answer differently.
|
|
1353
|
+
Object.defineProperty(module.exports, '_planInternals', {
|
|
1354
|
+
value: { Plan, deepEqual, cpAtLeast, cpAtMost, multipleOfOk, dataBits, typeBit, T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_OBJECT, T_ARRAY, T_ANY },
|
|
1355
|
+
enumerable: false,
|
|
1356
|
+
});
|