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