ata-validator 1.6.2 → 1.7.1

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.
@@ -4,9 +4,10 @@
4
4
  // schemas the JS codegen cannot represent when the native engine is absent
5
5
  // (browser, edge workers, ATA_NO_NATIVE). It walks the schema directly with
6
6
  // full annotation tracking (unevaluatedProperties/Items), $id/$anchor
7
- // resolution, and $dynamicRef dynamic-scope semantics. It optimizes for
8
- // correctness and clarity, not speed: hot paths belong to codegen and the
9
- // native engine.
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.
10
11
 
11
12
  const { compileSafe } = require('./safe-regex');
12
13
 
@@ -135,6 +136,10 @@ function walkPointer(root, pointer) {
135
136
  node = node.$defs;
136
137
  } else if (part === '$defs' && node.definitions) {
137
138
  node = node.definitions;
139
+ } else if (part === 'items' && Array.isArray(node.prefixItems)) {
140
+ // draft-07 array-form items is normalized to prefixItems; pointers
141
+ // written against the original document still land.
142
+ node = node.prefixItems;
138
143
  } else {
139
144
  return undefined;
140
145
  }
@@ -200,13 +205,27 @@ function deepEqual(a, b) {
200
205
  function codePointLength(s) {
201
206
  let n = 0;
202
207
  for (let i = 0; i < s.length; i++) {
203
- const c = s.charCodeAt(i);
204
- if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) i++;
208
+ // Wraparound test: one compare classifies a high surrogate.
209
+ if (((s.charCodeAt(i) - 0xd800) >>> 0) < 0x400 && i + 1 < s.length) i++;
205
210
  n++;
206
211
  }
207
212
  return n;
208
213
  }
209
214
 
215
+ // The code point count of a UTF-16 string sits between ceil(length / 2) and
216
+ // length, so most length bounds are decided without counting anything.
217
+ function cpAtLeast(s, min) {
218
+ if (s.length >= 2 * min) return true;
219
+ if (s.length < min) return false;
220
+ return codePointLength(s) >= min;
221
+ }
222
+
223
+ function cpAtMost(s, max) {
224
+ if (s.length <= max) return true;
225
+ if (s.length > 2 * max + 1) return false;
226
+ return codePointLength(s) <= max;
227
+ }
228
+
210
229
  function typeMatches(type, d) {
211
230
  switch (type) {
212
231
  case 'string': return typeof d === 'string';
@@ -246,7 +265,15 @@ const FORMAT_CHECKS = {
246
265
 
247
266
  // ---------- evaluation ----------
248
267
 
249
- const NO_ANNOTATIONS = { props: null, items: null };
268
+ // Sink for subschema results whose annotations the caller never reads
269
+ // (properties, items, propertyNames and friends). eval() skips the merge when
270
+ // it sees this object, so it is never mutated.
271
+ const DISCARD = { props: null, items: null };
272
+
273
+ // Verdict-only error sink. When eval() sees this identity it neither builds
274
+ // error objects nor formats messages; every push site is guarded on it. The
275
+ // array is frozen so a missed guard throws in tests instead of hiding.
276
+ const NOERRORS = Object.freeze([]);
250
277
 
251
278
  function mergeAnnotations(target, from) {
252
279
  if (from.props && from.props.size) {
@@ -259,6 +286,310 @@ function mergeAnnotations(target, from) {
259
286
  }
260
287
  }
261
288
 
289
+ // ---------- plans ----------
290
+
291
+ // Type bits, one per JSON Schema type. `integer` data also sets the `number`
292
+ // bit so a `number` constraint accepts it.
293
+ const T_STRING = 1;
294
+ const T_NUMBER = 2;
295
+ const T_INTEGER = 4;
296
+ const T_BOOLEAN = 8;
297
+ const T_NULL = 16;
298
+ const T_OBJECT = 32;
299
+ const T_ARRAY = 64;
300
+ const T_ANY = 127;
301
+
302
+ function typeBit(name) {
303
+ switch (name) {
304
+ case 'string': return T_STRING;
305
+ case 'number': return T_NUMBER;
306
+ case 'integer': return T_INTEGER;
307
+ case 'boolean': return T_BOOLEAN;
308
+ case 'null': return T_NULL;
309
+ case 'object': return T_OBJECT;
310
+ case 'array': return T_ARRAY;
311
+ default: return T_ANY; // unknown type names match everything
312
+ }
313
+ }
314
+
315
+ function dataBits(d) {
316
+ switch (typeof d) {
317
+ case 'string': return T_STRING;
318
+ case 'number':
319
+ if (!isFinite(d)) return 0;
320
+ return Number.isInteger(d) ? T_NUMBER | T_INTEGER : T_NUMBER;
321
+ case 'boolean': return T_BOOLEAN;
322
+ case 'object':
323
+ if (d === null) return T_NULL;
324
+ return Array.isArray(d) ? T_ARRAY : T_OBJECT;
325
+ default: return 0;
326
+ }
327
+ }
328
+
329
+ function isObjectMap(v) {
330
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
331
+ }
332
+
333
+ // A plan is the schema node with every keyword resolved once: the type list
334
+ // folded to a bitmask, the format looked up, patterns compiled, and a flag
335
+ // per section so eval() can skip whole blocks without touching the schema
336
+ // object. Plans have a fixed shape, which keeps eval() monomorphic where the
337
+ // raw schema objects are not.
338
+ class Plan {
339
+ // The plan is registered in the cache before `link` runs, so a schema
340
+ // object that contains itself structurally resolves to the same plan
341
+ // instead of recursing without end.
342
+ constructor(schema) {
343
+ this.schema = schema;
344
+ }
345
+
346
+ link(interp) {
347
+ const schema = this.schema;
348
+ // Children are linked as plans (or booleans) up front, so a visit never
349
+ // goes back through the plan cache. Reference targets are resolved at
350
+ // evaluation time and cached per base URI.
351
+ const child = (sub) => interp.node(sub);
352
+ this.refCache = null;
353
+ this.refBase0 = undefined;
354
+ this.refRes0 = null;
355
+ this.dynCache = null;
356
+ this.ref = typeof schema.$ref === 'string' ? schema.$ref : null;
357
+ this.dynamicRef = typeof schema.$dynamicRef === 'string' ? schema.$dynamicRef : null;
358
+ this.tracked = this.ref !== null || this.dynamicRef !== null;
359
+
360
+ this.typeMask = 0;
361
+ this.typeNames = null;
362
+ if (schema.type !== undefined) {
363
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
364
+ let mask = 0;
365
+ for (const t of types) mask |= typeBit(t);
366
+ this.typeMask = mask;
367
+ this.typeNames = types;
368
+ }
369
+ this.hasType = schema.type !== undefined;
370
+ this.enum = schema.enum !== undefined ? schema.enum : null;
371
+ this.hasConst = schema.const !== undefined;
372
+ this.const = schema.const;
373
+
374
+ this.minimum = typeof schema.minimum === 'number' ? schema.minimum : undefined;
375
+ this.maximum = typeof schema.maximum === 'number' ? schema.maximum : undefined;
376
+ this.exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined;
377
+ this.exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined;
378
+ this.multipleOf = typeof schema.multipleOf === 'number' ? schema.multipleOf : undefined;
379
+ this.hasNumber = this.minimum !== undefined || this.maximum !== undefined || this.exclusiveMinimum !== undefined || this.exclusiveMaximum !== undefined || this.multipleOf !== undefined;
380
+
381
+ this.minLength = schema.minLength;
382
+ this.maxLength = schema.maxLength;
383
+ this.pattern = schema.pattern !== undefined ? interp.pattern(schema.pattern) : null;
384
+ this.patternSource = schema.pattern;
385
+ this.format = schema.format;
386
+ this.formatFn = null;
387
+ if (schema.format !== undefined) {
388
+ const uf = interp.userFormats;
389
+ const fc = (uf && typeof uf[schema.format] === 'function') ? uf[schema.format] : FORMAT_CHECKS[schema.format];
390
+ this.formatFn = fc || null;
391
+ }
392
+ this.hasString = this.minLength !== undefined || this.maxLength !== undefined || this.pattern !== null || this.formatFn !== null;
393
+
394
+ this.minItems = schema.minItems;
395
+ this.maxItems = schema.maxItems;
396
+ this.uniqueItems = schema.uniqueItems === true;
397
+ this.prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems.map(child) : null;
398
+ this.items = schema.items !== undefined ? child(schema.items) : undefined;
399
+ this.contains = schema.contains !== undefined ? child(schema.contains) : undefined;
400
+ this.minContains = schema.minContains;
401
+ this.maxContains = schema.maxContains;
402
+ this.hasArray = this.minItems !== undefined || this.maxItems !== undefined || this.uniqueItems || this.prefixItems !== null || this.items !== undefined || this.contains !== undefined;
403
+
404
+ this.required = Array.isArray(schema.required) ? schema.required : null;
405
+ this.minProperties = schema.minProperties;
406
+ this.maxProperties = schema.maxProperties;
407
+ this.dependentRequired = isObjectMap(schema.dependentRequired) ? Object.entries(schema.dependentRequired) : null;
408
+ this.propertyNames = schema.propertyNames !== undefined ? child(schema.propertyNames) : undefined;
409
+ this.properties = null;
410
+ if (isObjectMap(schema.properties)) {
411
+ this.properties = new Map();
412
+ for (const key of Object.keys(schema.properties)) {
413
+ const ek = escapePointer(key);
414
+ // Escaped once here; the walk only concatenates.
415
+ this.properties.set(key, { node: child(schema.properties[key]), seg: '/' + ek, schemaSeg: '/properties/' + ek });
416
+ }
417
+ }
418
+ this.patternProperties = null;
419
+ if (isObjectMap(schema.patternProperties)) {
420
+ this.patternProperties = Object.keys(schema.patternProperties).map((src) => ({ src, re: interp.pattern(src), node: child(schema.patternProperties[src]) }));
421
+ }
422
+ this.additionalProperties = schema.additionalProperties !== undefined ? child(schema.additionalProperties) : undefined;
423
+ this.dependentSchemas = isObjectMap(schema.dependentSchemas) ? Object.entries(schema.dependentSchemas).map(([k, v]) => [k, child(v)]) : null;
424
+ this.propertyDependencies = null;
425
+ if (isObjectMap(schema.propertyDependencies)) {
426
+ this.propertyDependencies = Object.entries(schema.propertyDependencies)
427
+ .filter(([, choices]) => isObjectMap(choices))
428
+ .map(([k, choices]) => [k, new Map(Object.keys(choices).map((v) => [v, child(choices[v])]))]);
429
+ }
430
+ 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;
431
+
432
+ this.allOf = Array.isArray(schema.allOf) ? schema.allOf.map(child) : null;
433
+ this.anyOf = Array.isArray(schema.anyOf) ? schema.anyOf.map(child) : null;
434
+ this.oneOf = Array.isArray(schema.oneOf) ? schema.oneOf.map(child) : null;
435
+ this.not = schema.not !== undefined ? child(schema.not) : undefined;
436
+ this.if = schema.if !== undefined ? child(schema.if) : undefined;
437
+ this.then = schema.then !== undefined ? child(schema.then) : undefined;
438
+ this.else = schema.else !== undefined ? child(schema.else) : undefined;
439
+ this.hasApplicators = this.allOf !== null || this.anyOf !== null || this.oneOf !== null || this.not !== undefined || this.if !== undefined;
440
+
441
+ this.unevaluatedProperties = schema.unevaluatedProperties !== undefined ? child(schema.unevaluatedProperties) : undefined;
442
+ this.unevaluatedItems = schema.unevaluatedItems !== undefined ? child(schema.unevaluatedItems) : undefined;
443
+ this.hasUnevaluated = this.unevaluatedProperties !== undefined || this.unevaluatedItems !== undefined;
444
+
445
+ // Resource bookkeeping, read once instead of two Map lookups per visit.
446
+ const nodeBase = interp.state.nodeBase.get(schema);
447
+ this.nodeBase = nodeBase !== undefined ? nodeBase : null;
448
+ this.isResourceRoot = nodeBase !== undefined && interp.state.resources.get(nodeBase) === schema;
449
+
450
+ // A leaf plan checks only value-level keywords: no references, no
451
+ // subschemas, no annotations, no resource boundary. Those skip the whole
452
+ // eval() prologue (cycle guard, scope, annotation sinks) and run through
453
+ // evalLeaf(). Most schemas are mostly leaves.
454
+ this.leaf = !this.tracked && !this.hasApplicators && !this.hasUnevaluated &&
455
+ this.properties === null && this.patternProperties === null &&
456
+ this.additionalProperties === undefined && this.propertyNames === undefined &&
457
+ this.dependentSchemas === null && this.propertyDependencies === null &&
458
+ this.prefixItems === null && this.items === undefined && this.contains === undefined &&
459
+ this.nodeBase === null;
460
+ }
461
+ }
462
+
463
+
464
+ // Verdict-only twin of evalLeaf: no error objects, no path strings, first
465
+ // failure returns. Kept adjacent so the two cannot drift.
466
+ function evalLeafV(P, data) {
467
+ const bits = dataBits(data);
468
+ if (P.hasType && (bits & P.typeMask) === 0) return false;
469
+ if (P.enum !== null) {
470
+ let found = false;
471
+ for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
472
+ if (!found) return false;
473
+ }
474
+ if (P.hasConst && !deepEqual(P.const, data)) return false;
475
+ if (P.hasNumber && typeof data === 'number') {
476
+ if (P.minimum !== undefined && !(data >= P.minimum)) return false;
477
+ if (P.maximum !== undefined && !(data <= P.maximum)) return false;
478
+ if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
479
+ if (P.exclusiveMaximum !== undefined && !(data < P.exclusiveMaximum)) return false;
480
+ if (P.multipleOf !== undefined && !multipleOfOk(data, P.multipleOf)) return false;
481
+ }
482
+ if (P.hasString && bits === T_STRING) {
483
+ if (P.minLength !== undefined && !cpAtLeast(data, P.minLength)) return false;
484
+ if (P.maxLength !== undefined && !cpAtMost(data, P.maxLength)) return false;
485
+ if (P.pattern !== null && !P.pattern.test(data)) return false;
486
+ if (P.formatFn !== null && !P.formatFn(data)) return false;
487
+ }
488
+ if (P.hasArray && bits === T_ARRAY) {
489
+ if (P.minItems !== undefined && data.length < P.minItems) return false;
490
+ if (P.maxItems !== undefined && data.length > P.maxItems) return false;
491
+ if (P.uniqueItems) {
492
+ for (let i = 0; i < data.length; i++) {
493
+ for (let j = i + 1; j < data.length; j++) {
494
+ if (deepEqual(data[i], data[j])) return false;
495
+ }
496
+ }
497
+ }
498
+ }
499
+ if (P.hasObject && bits === T_OBJECT) {
500
+ if (P.required !== null) {
501
+ const req = P.required;
502
+ for (let i = 0; i < req.length; i++) {
503
+ if (!Object.hasOwn(data, req[i])) return false;
504
+ }
505
+ }
506
+ if (P.minProperties !== undefined && Object.keys(data).length < P.minProperties) return false;
507
+ if (P.maxProperties !== undefined && Object.keys(data).length > P.maxProperties) return false;
508
+ if (P.dependentRequired !== null) {
509
+ for (const [key, deps] of P.dependentRequired) {
510
+ if (Object.hasOwn(data, key)) {
511
+ for (const dep of deps) if (!Object.hasOwn(data, dep)) return false;
512
+ }
513
+ }
514
+ }
515
+ }
516
+ return true;
517
+ }
518
+
519
+ // Value-level checks only; the caller guarantees P.leaf. The bodies mirror
520
+ // the corresponding sections of eval() exactly, minus the prologue.
521
+ function evalLeaf(P, data, errors, instancePath, schemaPath) {
522
+ let valid = true;
523
+ const schema = P.schema;
524
+ const bits = dataBits(data);
525
+ if (P.hasType && (bits & P.typeMask) === 0) {
526
+ if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
527
+ valid = false;
528
+ }
529
+ if (P.enum !== null) {
530
+ let found = false;
531
+ for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
532
+ if (!found) {
533
+ if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
534
+ valid = false;
535
+ }
536
+ }
537
+ if (P.hasConst && !deepEqual(P.const, data)) {
538
+ if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
539
+ valid = false;
540
+ }
541
+ if (P.hasNumber && typeof data === 'number') {
542
+ 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; }
543
+ 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; }
544
+ 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; }
545
+ 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; }
546
+ 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; }
547
+ }
548
+ if (P.hasString && bits === T_STRING) {
549
+ 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; }
550
+ 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; }
551
+ 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; }
552
+ 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; }
553
+ }
554
+ if (P.hasArray && bits === T_ARRAY) {
555
+ 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; }
556
+ 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; }
557
+ if (P.uniqueItems) {
558
+ outer: for (let i = 0; i < data.length; i++) {
559
+ for (let j = i + 1; j < data.length; j++) {
560
+ if (deepEqual(data[i], data[j])) {
561
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
562
+ valid = false;
563
+ break outer;
564
+ }
565
+ }
566
+ }
567
+ }
568
+ }
569
+ if (P.hasObject && bits === T_OBJECT) {
570
+ const keys = Object.keys(data);
571
+ if (P.required !== null) {
572
+ const req = P.required;
573
+ for (let i = 0; i < req.length; i++) {
574
+ const key = req[i];
575
+ if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
576
+ }
577
+ }
578
+ 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; }
579
+ 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; }
580
+ if (P.dependentRequired !== null) {
581
+ for (const [key, deps] of P.dependentRequired) {
582
+ if (Object.hasOwn(data, key)) {
583
+ for (const dep of deps) {
584
+ if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
585
+ }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ return valid;
591
+ }
592
+
262
593
  class Interpreter {
263
594
  constructor(rootSchema, options) {
264
595
  const opts = options || {};
@@ -269,6 +600,21 @@ class Interpreter {
269
600
  // requirement. See the `$dynamicRef` branch in eval().
270
601
  this.bookending = !opts.v1;
271
602
  this.patternCache = new Map();
603
+ // Plans are built on first visit; a schema node is never re-read after.
604
+ this.plans = new Map();
605
+ this.rootNode = this.node(rootSchema);
606
+ // Compiled closure tree: undefined = not attempted, null = out of scope.
607
+ this._fast = undefined;
608
+ }
609
+
610
+ // The closure-tree compiler covers single-resource schemas without
611
+ // unevaluated keywords; everything else stays on eval(). Compiled lazily so
612
+ // construction stays cheap for schemas that never validate.
613
+ _fastRoot() {
614
+ if (this._fast === undefined) {
615
+ try { this._fast = compileInterpreter(this); } catch { this._fast = null; }
616
+ }
617
+ return this._fast;
272
618
  }
273
619
 
274
620
  pattern(src) {
@@ -290,57 +636,142 @@ class Interpreter {
290
636
  return re;
291
637
  }
292
638
 
639
+ plan(schema) {
640
+ let p = this.plans.get(schema);
641
+ if (p === undefined) {
642
+ p = new Plan(schema);
643
+ this.plans.set(schema, p);
644
+ p.link(this);
645
+ }
646
+ return p;
647
+ }
648
+
649
+ // $ref resolution depends on the base URI in effect, which can differ
650
+ // between visits of the same node; cache per (plan, base). Nearly every
651
+ // plan only ever sees one base, so the last resolution lives in two plain
652
+ // fields and the Map exists only for the rare second base. The cached
653
+ // entry carries the already-planned child, so a visit does no Map lookups.
654
+ resolveRefCached(P, base) {
655
+ if (P.refBase0 === base) return P.refRes0;
656
+ let cache = P.refCache;
657
+ let r = cache !== null ? cache.get(base) : undefined;
658
+ if (r === undefined) {
659
+ const raw = resolveRef(P.ref, base, this.state);
660
+ r = { child: raw.node === undefined ? undefined : this.node(raw.node), base: raw.base };
661
+ if (P.refBase0 === undefined) { P.refBase0 = base; P.refRes0 = r; }
662
+ else { if (cache === null) cache = P.refCache = new Map(); cache.set(base, r); }
663
+ }
664
+ return r;
665
+ }
666
+
667
+ // The static half of $dynamicRef (initial target and fragment) per base;
668
+ // the dynamic-scope search still runs on every visit.
669
+ resolveDynamicRefCached(P, base) {
670
+ let cache = P.dynCache;
671
+ if (cache === null) cache = P.dynCache = new Map();
672
+ let r = cache.get(base);
673
+ if (r === undefined) {
674
+ const { node, base: refBase } = resolveRef(P.dynamicRef, base, this.state);
675
+ const [, fragment] = splitFragment(resolveUri(base, P.dynamicRef));
676
+ r = { node, base: refBase, fragment, byAnchor: Boolean(fragment) && !fragment.startsWith('/') };
677
+ cache.set(base, r);
678
+ }
679
+ return r;
680
+ }
681
+
682
+ // Maps any schema value to what eval() accepts: a boolean or a Plan.
683
+ // Non-object, non-boolean values validate everything, as before.
684
+ node(schema) {
685
+ if (schema === true || schema === false) return schema;
686
+ if (typeof schema !== 'object' || schema === null) return true;
687
+ return this.plan(schema);
688
+ }
689
+
690
+ // Verdict only: same walk as validate(), no error construction at all.
691
+ isValid(data) {
692
+ const fast = this._fastRoot();
693
+ if (fast !== null) return fast.v(data, []);
694
+ const dynScope = [this.state.rootBase];
695
+ return this.eval(this.rootNode, data, this.state.rootBase, dynScope, NOERRORS, '', '#', [], DISCARD);
696
+ }
697
+
293
698
  validate(data) {
699
+ const fast = this._fastRoot();
700
+ if (fast !== null) {
701
+ const errors = [];
702
+ const valid = fast.c(data, errors, '', '#', []);
703
+ return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
704
+ }
294
705
  const errors = [];
295
706
  const dynScope = [this.state.rootBase];
296
- const valid = this.eval(this.root, data, this.state.rootBase, dynScope, errors, '', '#', [], NO_ANNOTATIONS_SINK());
707
+ // Nothing reads the root's annotations, so they are discarded from the start.
708
+ const valid = this.eval(this.rootNode, data, this.state.rootBase, dynScope, errors, '', '#', [], DISCARD);
297
709
  return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
298
710
  }
299
711
 
300
712
  // Evaluates `schema` against `data`. Collected annotations for data are
301
713
  // written into `sink` ({props, items}); errors append to `errors`.
302
714
  // `stack` breaks infinite $ref recursion on identical (schema, data) pairs.
303
- eval(schema, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
304
- if (schema === true) return true;
305
- if (schema === false) {
306
- errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
715
+ eval(P, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
716
+ if (P === true) return true;
717
+ if (P === false) {
718
+ if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
307
719
  return false;
308
720
  }
309
- if (typeof schema !== 'object' || schema === null) return true;
721
+ if (P.leaf) return evalLeaf(P, data, errors, instancePath, schemaPath);
722
+ const schema = P.schema;
310
723
 
311
- for (const frame of stack) {
312
- if (frame[0] === schema && frame[1] === data) return true; // fixed point
724
+ // Only a reference can lead evaluation back to a schema it is already
725
+ // inside of, so the cycle guard is maintained on reference nodes alone.
726
+ // The frame is pushed in place and popped on the way out; the array is
727
+ // created per validate() call, so a throw leaves nothing to clean up.
728
+ const tracked = P.tracked;
729
+ if (tracked) {
730
+ for (let i = stack.length - 2; i >= 0; i -= 2) {
731
+ if (stack[i] === schema && stack[i + 1] === data) return true; // fixed point
732
+ }
733
+ stack.push(schema, data);
313
734
  }
314
- stack = stack.concat([[schema, data]]);
315
735
 
316
- const nodeBase = this.state.nodeBase.get(schema);
317
- if (nodeBase !== undefined && nodeBase !== base) base = nodeBase;
318
- // Entering a schema resource extends the dynamic scope.
319
- if (this.state.resources.get(base) === schema && dynScope[dynScope.length - 1] !== base) {
320
- dynScope = dynScope.concat(base);
736
+ if (P.nodeBase !== null && P.nodeBase !== base) base = P.nodeBase;
737
+ // Entering a schema resource extends the dynamic scope, whether evaluation
738
+ // lands on the resource's root or on a subschema inside it: a reference
739
+ // to first#/$defs/stuff still passes through resource `first`. The scope
740
+ // is pushed in place and popped on the way out; evaluation is depth-first
741
+ // and nothing retains the array.
742
+ let scopePushed = false;
743
+ if (dynScope[dynScope.length - 1] !== base) {
744
+ dynScope.push(base);
745
+ scopePushed = true;
321
746
  }
322
747
 
323
748
  let valid = true;
324
- const local = { props: null, items: null };
749
+ // Annotations only matter if this node or an ancestor reads them. When
750
+ // nobody does, in-place children get the discard sink too and `local`
751
+ // stays unallocated.
752
+ const collect = sink !== DISCARD || P.hasUnevaluated;
753
+ const local = collect ? { props: null, items: null } : DISCARD;
325
754
 
326
755
  // $ref / $dynamicRef: in-place applicators, annotations flow through
327
- if (typeof schema.$ref === 'string') {
328
- const { node, base: refBase } = resolveRef(schema.$ref, base, this.state);
329
- if (node === undefined) {
330
- errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: schema.$ref }, `cannot resolve $ref ${schema.$ref}`));
756
+ if (P.ref !== null) {
757
+ const { child, base: refBase } = this.resolveRefCached(P, base);
758
+ if (child === undefined) {
759
+ if (errors !== NOERRORS) errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: P.ref }, `cannot resolve $ref ${P.ref}`));
331
760
  valid = false;
332
761
  } else {
333
- const sub = { props: null, items: null };
334
- if (!this.eval(node, data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
335
- else mergeAnnotations(local, sub);
762
+ const sub = collect ? { props: null, items: null } : DISCARD;
763
+ if (!this.eval(child, data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
764
+ else if (collect) mergeAnnotations(local, sub);
336
765
  }
337
766
  }
338
767
 
339
- if (typeof schema.$dynamicRef === 'string') {
340
- const ref = schema.$dynamicRef;
341
- let { node, base: refBase } = resolveRef(ref, base, this.state);
342
- const [, fragment] = splitFragment(resolveUri(base, ref));
343
- if (fragment && !fragment.startsWith('/')) {
768
+ if (P.dynamicRef !== null) {
769
+ const ref = P.dynamicRef;
770
+ const initial = this.resolveDynamicRefCached(P, base);
771
+ let node = initial.node;
772
+ let refBase = initial.base;
773
+ const fragment = initial.fragment;
774
+ if (initial.byAnchor) {
344
775
  // Under 2020-12 the initial target must itself carry the matching
345
776
  // $dynamicAnchor before the dynamic scope is consulted (bookending);
346
777
  // otherwise the keyword behaves like $ref. v1 removes that
@@ -364,100 +795,92 @@ class Interpreter {
364
795
  }
365
796
  }
366
797
  if (node === undefined) {
367
- errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
798
+ if (errors !== NOERRORS) errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
368
799
  valid = false;
369
800
  } else {
370
- const sub = { props: null, items: null };
371
- if (!this.eval(node, data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
372
- else mergeAnnotations(local, sub);
801
+ const sub = collect ? { props: null, items: null } : DISCARD;
802
+ if (!this.eval(this.node(node), data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
803
+ else if (collect) mergeAnnotations(local, sub);
373
804
  }
374
805
  }
375
806
 
807
+ const bits = dataBits(data);
808
+
376
809
  // type / enum / const
377
- if (schema.type !== undefined) {
378
- const types = Array.isArray(schema.type) ? schema.type : [schema.type];
379
- if (!types.some((t) => typeMatches(t, data))) {
380
- errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${types.join(' or ')}`));
381
- valid = false;
382
- }
810
+ if (P.hasType && (bits & P.typeMask) === 0) {
811
+ if (errors !== NOERRORS) errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${P.typeNames.join(' or ')}`));
812
+ valid = false;
383
813
  }
384
- if (schema.enum !== undefined) {
385
- if (!schema.enum.some((v) => deepEqual(v, data))) {
386
- errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: schema.enum }, 'must be equal to one of the allowed values'));
814
+ if (P.enum !== null) {
815
+ let found = false;
816
+ for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
817
+ if (!found) {
818
+ if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
387
819
  valid = false;
388
820
  }
389
821
  }
390
- if (schema.const !== undefined) {
391
- if (!deepEqual(schema.const, data)) {
392
- errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: schema.const }, 'must be equal to constant'));
393
- valid = false;
394
- }
822
+ if (P.hasConst && !deepEqual(P.const, data)) {
823
+ if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
824
+ valid = false;
395
825
  }
396
826
 
397
827
  // numbers
398
- if (typeof data === 'number') {
399
- if (typeof schema.minimum === 'number' && !(data >= schema.minimum)) { errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: schema.minimum }, `must be >= ${schema.minimum}`)); valid = false; }
400
- if (typeof schema.maximum === 'number' && !(data <= schema.maximum)) { errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: schema.maximum }, `must be <= ${schema.maximum}`)); valid = false; }
401
- if (typeof schema.exclusiveMinimum === 'number' && !(data > schema.exclusiveMinimum)) { errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: schema.exclusiveMinimum }, `must be > ${schema.exclusiveMinimum}`)); valid = false; }
402
- if (typeof schema.exclusiveMaximum === 'number' && !(data < schema.exclusiveMaximum)) { errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: schema.exclusiveMaximum }, `must be < ${schema.exclusiveMaximum}`)); valid = false; }
403
- if (typeof schema.multipleOf === 'number' && !multipleOfOk(data, schema.multipleOf)) { errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: schema.multipleOf }, `must be multiple of ${schema.multipleOf}`)); valid = false; }
828
+ if (P.hasNumber && typeof data === 'number') {
829
+ 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; }
830
+ 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; }
831
+ 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; }
832
+ 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; }
833
+ 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; }
404
834
  }
405
835
 
406
836
  // strings
407
- if (typeof data === 'string') {
408
- if (schema.minLength !== undefined && codePointLength(data) < schema.minLength) { errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: schema.minLength }, `must NOT have fewer than ${schema.minLength} characters`)); valid = false; }
409
- if (schema.maxLength !== undefined && codePointLength(data) > schema.maxLength) { errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: schema.maxLength }, `must NOT have more than ${schema.maxLength} characters`)); valid = false; }
410
- if (schema.pattern !== undefined && !this.pattern(schema.pattern).test(data)) { errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: schema.pattern }, `must match pattern "${schema.pattern}"`)); valid = false; }
411
- if (schema.format !== undefined) {
412
- const fc = (this.userFormats && typeof this.userFormats[schema.format] === 'function') ? this.userFormats[schema.format] : FORMAT_CHECKS[schema.format];
413
- if (fc && !fc(data)) { errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: schema.format }, `must match format "${schema.format}"`)); valid = false; }
414
- }
837
+ if (P.hasString && bits === T_STRING) {
838
+ 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; }
839
+ 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; }
840
+ 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; }
841
+ 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; }
415
842
  }
416
843
 
417
844
  // arrays
418
- if (Array.isArray(data)) {
419
- if (schema.minItems !== undefined && data.length < schema.minItems) { errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: schema.minItems }, `must NOT have fewer than ${schema.minItems} items`)); valid = false; }
420
- if (schema.maxItems !== undefined && data.length > schema.maxItems) { errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: schema.maxItems }, `must NOT have more than ${schema.maxItems} items`)); valid = false; }
421
- if (schema.uniqueItems === true) {
845
+ if (P.hasArray && bits === T_ARRAY) {
846
+ 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; }
847
+ 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; }
848
+ if (P.uniqueItems) {
422
849
  outer: for (let i = 0; i < data.length; i++) {
423
850
  for (let j = i + 1; j < data.length; j++) {
424
851
  if (deepEqual(data[i], data[j])) {
425
- errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
852
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
426
853
  valid = false;
427
854
  break outer;
428
855
  }
429
856
  }
430
857
  }
431
858
  }
432
- const prefix = Array.isArray(schema.prefixItems) ? schema.prefixItems : null;
433
- if (prefix) {
434
- for (let i = 0; i < Math.min(prefix.length, data.length); i++) {
435
- const sub = { props: null, items: null };
436
- if (!this.eval(prefix[i], data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack, sub)) valid = false;
437
- if (!local.items) local.items = new Set();
438
- local.items.add(i);
859
+ const prefix = P.prefixItems;
860
+ if (prefix !== null) {
861
+ const n = Math.min(prefix.length, data.length);
862
+ for (let i = 0; i < n; i++) {
863
+ if (!this.eval(prefix[i], data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack, DISCARD)) valid = false;
864
+ if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
439
865
  }
440
866
  }
441
- if (schema.items !== undefined) {
442
- const start = prefix ? prefix.length : 0;
867
+ if (P.items !== undefined) {
868
+ const start = prefix !== null ? prefix.length : 0;
443
869
  for (let i = start; i < data.length; i++) {
444
- const sub = { props: null, items: null };
445
- if (!this.eval(schema.items, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/items', stack, sub)) valid = false;
446
- if (!local.items) local.items = new Set();
447
- local.items.add(i);
870
+ if (!this.eval(P.items, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/items', stack, DISCARD)) valid = false;
871
+ if (collect) { if (!local.items) local.items = new Set(); local.items.add(i); }
448
872
  }
449
873
  }
450
- if (schema.contains !== undefined) {
874
+ if (P.contains !== undefined) {
451
875
  const matched = [];
452
876
  for (let i = 0; i < data.length; i++) {
453
- const scratch = [];
454
- const sub = { props: null, items: null };
455
- if (this.eval(schema.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, sub)) matched.push(i);
877
+ const scratch = errors === NOERRORS ? NOERRORS : [];
878
+ if (this.eval(P.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, DISCARD)) matched.push(i);
456
879
  }
457
- const minC = schema.minContains !== undefined ? schema.minContains : 1;
458
- if (matched.length < minC) { errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
459
- if (schema.maxContains !== undefined && matched.length > schema.maxContains) { errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: schema.maxContains }, `must NOT contain more than ${schema.maxContains} valid item(s)`)); valid = false; }
460
- if (matched.length) {
880
+ const minC = P.minContains !== undefined ? P.minContains : 1;
881
+ 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; }
882
+ if (P.maxContains !== undefined && matched.length > P.maxContains) { errors === NOERRORS || errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: P.maxContains }, `must NOT contain more than ${P.maxContains} valid item(s)`)); valid = false; }
883
+ if (collect && matched.length) {
461
884
  if (!local.items) local.items = new Set();
462
885
  for (const i of matched) local.items.add(i);
463
886
  }
@@ -465,63 +888,65 @@ class Interpreter {
465
888
  }
466
889
 
467
890
  // objects
468
- if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
891
+ if (P.hasObject && bits === T_OBJECT) {
469
892
  const keys = Object.keys(data);
470
- if (Array.isArray(schema.required)) {
471
- for (const key of schema.required) {
472
- if (!Object.prototype.hasOwnProperty.call(data, key)) { errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
893
+ if (P.required !== null) {
894
+ const req = P.required;
895
+ for (let i = 0; i < req.length; i++) {
896
+ const key = req[i];
897
+ if (!Object.hasOwn(data, key)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
473
898
  }
474
899
  }
475
- if (schema.minProperties !== undefined && keys.length < schema.minProperties) { errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: schema.minProperties }, `must NOT have fewer than ${schema.minProperties} properties`)); valid = false; }
476
- if (schema.maxProperties !== undefined && keys.length > schema.maxProperties) { errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: schema.maxProperties }, `must NOT have more than ${schema.maxProperties} properties`)); valid = false; }
477
- if (schema.dependentRequired && typeof schema.dependentRequired === 'object') {
478
- for (const [key, deps] of Object.entries(schema.dependentRequired)) {
479
- if (Object.prototype.hasOwnProperty.call(data, key)) {
900
+ 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; }
901
+ 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; }
902
+ if (P.dependentRequired !== null) {
903
+ for (const [key, deps] of P.dependentRequired) {
904
+ if (Object.hasOwn(data, key)) {
480
905
  for (const dep of deps) {
481
- if (!Object.prototype.hasOwnProperty.call(data, dep)) { errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
906
+ if (!Object.hasOwn(data, dep)) { errors === NOERRORS || errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
482
907
  }
483
908
  }
484
909
  }
485
910
  }
486
- if (schema.propertyNames !== undefined) {
911
+ if (P.propertyNames !== undefined) {
487
912
  for (const key of keys) {
488
- if (!this.eval(schema.propertyNames, key, base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack, { props: null, items: null })) valid = false;
913
+ if (!this.eval(P.propertyNames, key, base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack, DISCARD)) valid = false;
489
914
  }
490
915
  }
491
- const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : null;
492
- const patterns = schema.patternProperties && typeof schema.patternProperties === 'object' ? Object.keys(schema.patternProperties) : null;
493
- for (const key of keys) {
916
+ const props = P.properties;
917
+ const patterns = P.patternProperties;
918
+ for (let k = 0; k < keys.length; k++) {
919
+ const key = keys[k];
494
920
  let evaluated = false;
495
- if (props && Object.prototype.hasOwnProperty.call(props, key)) {
496
- const sub = { props: null, items: null };
497
- if (!this.eval(props[key], data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/properties/' + escapePointer(key), stack, sub)) valid = false;
921
+ const prop = props !== null ? props.get(key) : undefined;
922
+ if (prop !== undefined) {
923
+ if (!this.eval(prop.node, data[key], base, dynScope, errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack, DISCARD)) valid = false;
498
924
  evaluated = true;
499
925
  }
500
- if (patterns) {
501
- for (const src of patterns) {
502
- if (this.pattern(src).test(key)) {
503
- const sub = { props: null, items: null };
504
- if (!this.eval(schema.patternProperties[src], data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(src), stack, sub)) valid = false;
926
+ if (patterns !== null) {
927
+ for (let pi = 0; pi < patterns.length; pi++) {
928
+ const pp = patterns[pi];
929
+ if (pp.re.test(key)) {
930
+ if (!this.eval(pp.node, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack, DISCARD)) valid = false;
505
931
  evaluated = true;
506
932
  }
507
933
  }
508
934
  }
509
- if (!evaluated && schema.additionalProperties !== undefined) {
510
- const sub = { props: null, items: null };
511
- if (!this.eval(schema.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, sub)) valid = false;
935
+ if (!evaluated && P.additionalProperties !== undefined) {
936
+ if (!this.eval(P.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, DISCARD)) valid = false;
512
937
  evaluated = true;
513
938
  }
514
- if (evaluated) {
939
+ if (evaluated && collect) {
515
940
  if (!local.props) local.props = new Set();
516
941
  local.props.add(key);
517
942
  }
518
943
  }
519
- if (schema.dependentSchemas && typeof schema.dependentSchemas === 'object') {
520
- for (const [key, dep] of Object.entries(schema.dependentSchemas)) {
521
- if (Object.prototype.hasOwnProperty.call(data, key)) {
522
- const sub = { props: null, items: null };
944
+ if (P.dependentSchemas !== null) {
945
+ for (const [key, dep] of P.dependentSchemas) {
946
+ if (Object.hasOwn(data, key)) {
947
+ const sub = collect ? { props: null, items: null } : DISCARD;
523
948
  if (!this.eval(dep, data, base, dynScope, errors, instancePath, schemaPath + '/dependentSchemas/' + escapePointer(key), stack, sub)) valid = false;
524
- else mergeAnnotations(local, sub);
949
+ else if (collect) mergeAnnotations(local, sub);
525
950
  }
526
951
  }
527
952
  }
@@ -529,125 +954,136 @@ class Interpreter {
529
954
  // value rather than its presence. Only string values can match, since the
530
955
  // inner keys are object keys. Equivalent to
531
956
  // `if: { properties: { p: { const: v } }, required: [p] }, then: <schema>`.
532
- if (schema.propertyDependencies && typeof schema.propertyDependencies === 'object') {
533
- for (const [key, choices] of Object.entries(schema.propertyDependencies)) {
534
- if (!choices || typeof choices !== 'object' || Array.isArray(choices)) continue;
535
- if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
957
+ if (P.propertyDependencies !== null) {
958
+ for (const [key, choices] of P.propertyDependencies) {
959
+ if (!Object.hasOwn(data, key)) continue;
536
960
  const value = data[key];
537
961
  if (typeof value !== 'string') continue;
538
- if (!Object.prototype.hasOwnProperty.call(choices, value)) continue;
539
- const sub = { props: null, items: null };
962
+ const choice = choices.get(value);
963
+ if (choice === undefined) continue;
964
+ const sub = collect ? { props: null, items: null } : DISCARD;
540
965
  const branchPath = schemaPath + '/propertyDependencies/' + escapePointer(key) + '/' + escapePointer(value);
541
- if (!this.eval(choices[value], data, base, dynScope, errors, instancePath, branchPath, stack, sub)) valid = false;
542
- else mergeAnnotations(local, sub);
966
+ if (!this.eval(choice, data, base, dynScope, errors, instancePath, branchPath, stack, sub)) valid = false;
967
+ else if (collect) mergeAnnotations(local, sub);
543
968
  }
544
969
  }
545
970
  }
546
971
 
547
972
  // in-place applicators
548
- if (Array.isArray(schema.allOf)) {
549
- for (let i = 0; i < schema.allOf.length; i++) {
550
- const sub = { props: null, items: null };
551
- if (!this.eval(schema.allOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/allOf/' + i, stack, sub)) valid = false;
552
- else mergeAnnotations(local, sub);
553
- }
554
- }
555
- if (Array.isArray(schema.anyOf)) {
556
- let any = false;
557
- const scratch = [];
558
- for (let i = 0; i < schema.anyOf.length; i++) {
559
- const sub = { props: null, items: null };
560
- if (this.eval(schema.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
561
- any = true;
562
- mergeAnnotations(local, sub);
973
+ if (P.hasApplicators) {
974
+ if (P.allOf !== null) {
975
+ for (let i = 0; i < P.allOf.length; i++) {
976
+ const sub = collect ? { props: null, items: null } : DISCARD;
977
+ if (!this.eval(P.allOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/allOf/' + i, stack, sub)) valid = false;
978
+ else if (collect) mergeAnnotations(local, sub);
563
979
  }
564
980
  }
565
- if (!any) {
566
- for (const e of scratch) errors.push(e);
567
- errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
568
- valid = false;
569
- }
570
- }
571
- if (Array.isArray(schema.oneOf)) {
572
- let count = 0;
573
- let winner = null;
574
- const scratch = [];
575
- for (let i = 0; i < schema.oneOf.length; i++) {
576
- const sub = { props: null, items: null };
577
- if (this.eval(schema.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
578
- count++;
579
- winner = sub;
981
+ if (P.anyOf !== null) {
982
+ let any = false;
983
+ const scratch = errors === NOERRORS ? NOERRORS : [];
984
+ for (let i = 0; i < P.anyOf.length; i++) {
985
+ const sub = collect ? { props: null, items: null } : DISCARD;
986
+ if (this.eval(P.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
987
+ any = true;
988
+ if (collect) mergeAnnotations(local, sub);
989
+ }
990
+ }
991
+ if (!any) {
992
+ if (errors !== NOERRORS) for (const e of scratch) errors.push(e);
993
+ if (errors !== NOERRORS) errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
994
+ valid = false;
580
995
  }
581
996
  }
582
- if (count === 1) mergeAnnotations(local, winner);
583
- else {
584
- if (count === 0) for (const e of scratch) errors.push(e);
585
- errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
586
- valid = false;
997
+ if (P.oneOf !== null) {
998
+ let count = 0;
999
+ let winner = null;
1000
+ const scratch = errors === NOERRORS ? NOERRORS : [];
1001
+ for (let i = 0; i < P.oneOf.length; i++) {
1002
+ const sub = collect ? { props: null, items: null } : DISCARD;
1003
+ if (this.eval(P.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
1004
+ count++;
1005
+ winner = sub;
1006
+ }
1007
+ }
1008
+ if (count === 1) { if (collect) mergeAnnotations(local, winner); }
1009
+ else {
1010
+ if (count === 0) for (const e of scratch) errors.push(e);
1011
+ if (errors !== NOERRORS) errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
1012
+ valid = false;
1013
+ }
587
1014
  }
588
- }
589
- if (schema.not !== undefined) {
590
- const scratch = [];
591
- if (this.eval(schema.not, data, base, dynScope, scratch, instancePath, schemaPath + '/not', stack, { props: null, items: null })) {
592
- errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
593
- valid = false;
1015
+ if (P.not !== undefined) {
1016
+ const scratch = errors === NOERRORS ? NOERRORS : [];
1017
+ if (this.eval(P.not, data, base, dynScope, scratch, instancePath, schemaPath + '/not', stack, DISCARD)) {
1018
+ if (errors !== NOERRORS) errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
1019
+ valid = false;
1020
+ }
594
1021
  }
595
- }
596
- if (schema.if !== undefined) {
597
- const ifSub = { props: null, items: null };
598
- const ifScratch = [];
599
- const ifOk = this.eval(schema.if, data, base, dynScope, ifScratch, instancePath, schemaPath + '/if', stack, ifSub);
600
- if (ifOk) {
601
- mergeAnnotations(local, ifSub);
602
- if (schema.then !== undefined) {
603
- const sub = { props: null, items: null };
604
- if (!this.eval(schema.then, data, base, dynScope, errors, instancePath, schemaPath + '/then', stack, sub)) valid = false;
605
- else mergeAnnotations(local, sub);
1022
+ if (P.if !== undefined) {
1023
+ const ifSub = collect ? { props: null, items: null } : DISCARD;
1024
+ const ifScratch = errors === NOERRORS ? NOERRORS : [];
1025
+ const ifOk = this.eval(P.if, data, base, dynScope, ifScratch, instancePath, schemaPath + '/if', stack, ifSub);
1026
+ if (ifOk) {
1027
+ if (collect) mergeAnnotations(local, ifSub);
1028
+ if (P.then !== undefined) {
1029
+ const sub = collect ? { props: null, items: null } : DISCARD;
1030
+ if (!this.eval(P.then, data, base, dynScope, errors, instancePath, schemaPath + '/then', stack, sub)) valid = false;
1031
+ else if (collect) mergeAnnotations(local, sub);
1032
+ }
1033
+ } else if (P.else !== undefined) {
1034
+ const sub = collect ? { props: null, items: null } : DISCARD;
1035
+ if (!this.eval(P.else, data, base, dynScope, errors, instancePath, schemaPath + '/else', stack, sub)) valid = false;
1036
+ else if (collect) mergeAnnotations(local, sub);
606
1037
  }
607
- } else if (schema.else !== undefined) {
608
- const sub = { props: null, items: null };
609
- if (!this.eval(schema.else, data, base, dynScope, errors, instancePath, schemaPath + '/else', stack, sub)) valid = false;
610
- else mergeAnnotations(local, sub);
611
1038
  }
612
1039
  }
613
1040
 
614
1041
  // unevaluated*: run last, against annotations from everything above
615
- if (schema.unevaluatedProperties !== undefined && typeof data === 'object' && data !== null && !Array.isArray(data)) {
616
- for (const key of Object.keys(data)) {
617
- if (local.props && local.props.has(key)) continue;
618
- const sub = { props: null, items: null };
619
- if (!this.eval(schema.unevaluatedProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack, sub)) valid = false;
620
- if (!local.props) local.props = new Set();
621
- local.props.add(key);
1042
+ if (P.hasUnevaluated) {
1043
+ if (P.unevaluatedProperties !== undefined && bits === T_OBJECT) {
1044
+ for (const key of Object.keys(data)) {
1045
+ if (local.props && local.props.has(key)) continue;
1046
+ if (!this.eval(P.unevaluatedProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack, DISCARD)) valid = false;
1047
+ if (!local.props) local.props = new Set();
1048
+ local.props.add(key);
1049
+ }
622
1050
  }
623
- }
624
- if (schema.unevaluatedItems !== undefined && Array.isArray(data)) {
625
- for (let i = 0; i < data.length; i++) {
626
- if (local.items && local.items.has(i)) continue;
627
- const sub = { props: null, items: null };
628
- if (!this.eval(schema.unevaluatedItems, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack, sub)) valid = false;
629
- if (!local.items) local.items = new Set();
630
- local.items.add(i);
1051
+ if (P.unevaluatedItems !== undefined && bits === T_ARRAY) {
1052
+ for (let i = 0; i < data.length; i++) {
1053
+ if (local.items && local.items.has(i)) continue;
1054
+ if (!this.eval(P.unevaluatedItems, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack, DISCARD)) valid = false;
1055
+ if (!local.items) local.items = new Set();
1056
+ local.items.add(i);
1057
+ }
631
1058
  }
632
1059
  }
633
1060
 
634
- if (valid) mergeAnnotations(sink, local);
1061
+ if (scopePushed) dynScope.pop();
1062
+ if (tracked) stack.length -= 2;
1063
+ if (valid && collect && sink !== DISCARD) mergeAnnotations(sink, local);
635
1064
  return valid;
636
1065
  }
637
1066
  }
638
1067
 
639
- function NO_ANNOTATIONS_SINK() {
640
- return { props: null, items: null };
641
- }
642
-
643
1068
  function escapePointer(s) {
644
- return String(s).replace(/~/g, '~0').replace(/\//g, '~1');
1069
+ if (typeof s !== 'string') s = String(s);
1070
+ // Keys almost never contain `~` or `/`; skip the two regex passes when so.
1071
+ for (let i = 0; i < s.length; i++) {
1072
+ const c = s.charCodeAt(i);
1073
+ if (c === 0x7e || c === 0x2f) return s.replace(/~/g, '~0').replace(/\//g, '~1');
1074
+ }
1075
+ return s;
645
1076
  }
646
1077
 
647
1078
  function err(code, keyword, instancePath, schemaPath, params, message) {
648
1079
  return { keyword, instancePath, schemaPath, params, message };
649
1080
  }
650
1081
 
1082
+ const { compileInterpreter } = require('./plan-compiler').install({
1083
+ Plan, NOERRORS, err, evalLeaf, evalLeafV, deepEqual, dataBits, escapePointer,
1084
+ T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
1085
+ });
1086
+
651
1087
  function createInterpreter(schema, options) {
652
1088
  return new Interpreter(schema, options);
653
1089
  }