ata-validator 1.44.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -244,21 +244,21 @@ is the most common way to get a misleading number out of this library.
244
244
 
245
245
  | | compiled with `ata build` | runtime `new Validator(schema)` |
246
246
  |---|---|---|
247
- | In a bundle, gzipped | **2.1 KB** | 110.7 KB |
248
- | Time to a served request | **3.5 ms** | 8.2 ms |
247
+ | In a bundle, gzipped | **2.1 KB** | 113.3 KB |
248
+ | Time to a served request | **3.5 ms** | 8.5 ms |
249
249
  | Schema known when | build time | any time |
250
250
 
251
251
  The bundle row is the ten-field user schema in
252
252
  `tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
253
- `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.43.0.
253
+ `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.45.0.
254
254
  The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
255
- `benchmark/bundle`, against 3.4 ms for the same app doing no validation at all, so the
255
+ `benchmark/bundle`, against 3.7 ms for the same app doing no validation at all, so the
256
256
  compiled path costs nothing measurable to start. The runtime figure is what it is because
257
257
  a schema that arrives at run time can use any keyword, so the whole engine has to be
258
258
  there. The compiled module imports nothing and contains only the checks your schema asks
259
259
  for.
260
260
 
261
- **On a server, use whichever fits your schemas.** 111 KB of JavaScript on a Node or Bun
261
+ **On a server, use whichever fits your schemas.** 113 KB of JavaScript on a Node or Bun
262
262
  process is not a cost anyone notices, and the runtime API is the simpler thing to reach
263
263
  for. Speed is the same either way once warm.
264
264
 
@@ -278,7 +278,7 @@ const v = new Validator(schema)
278
278
  const result = v.validate(data)
279
279
  ```
280
280
 
281
- The runtime API is unchanged from previous releases. Code written against the default validator's class keeps working through `ata-validator/compat`, which covers `compile`, `addSchema`, `addFormat`, `addKeyword`, `errorsText` and the rest of that surface, and reports errors in the same shape and order. See [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
281
+ The runtime API is unchanged from previous releases. Code written against the default validator's class keeps working through `ata-validator/compat`, which covers `compile`, `addSchema`, `addFormat`, `addKeyword`, `errorsText` and the rest of that surface, and reports errors in the same shape and order. `npx ata migrate` reports what the switch would change in a project, and what it cannot translate, before anything is written. See [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
282
282
 
283
283
  ## Usage
284
284
 
@@ -491,7 +491,7 @@ const v = new Validator(schema, {
491
491
 
492
492
  ### Build-time compile (`ata compile`)
493
493
 
494
- The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 110.7 KB for the runtime bundled for the browser (ata 1.43.0).
494
+ The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 113.3 KB for the runtime bundled for the browser (ata 1.45.0).
495
495
 
496
496
  ```bash
497
497
  npx ata compile schemas/user.json -o src/generated/user.validator.mjs
@@ -533,11 +533,11 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
533
533
  Run with `--watch` during development for incremental rebuilds.
534
534
 
535
535
  Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
536
- minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.43.0:
536
+ minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.45.0:
537
537
 
538
538
  | What the app imports | Size | Notes |
539
539
  |---|---|---|
540
- | `Validator` from `ata-validator` | 110.7 KB | The compiler ships with it, because a runtime schema can use any keyword |
540
+ | `Validator` from `ata-validator` | 113.3 KB | The compiler ships with it, because a runtime schema can use any keyword |
541
541
  | `isValid` from the compiled module | **1.3 KB** | Nothing else is reachable, so the error collector is dropped |
542
542
  | `validate` from the compiled module | **1.9 KB** | Adds the detailed error collector |
543
543
 
@@ -575,7 +575,7 @@ through a `setFormats()` export the module carries. `docs/API.md` has the
575
575
  details.
576
576
 
577
577
  **Fastify startup, 10 route schemas, from a cold process to the first validated request:
578
- ajv 19.9 ms, ata 2.9 ms, no build step required.** ata registers in 0.5 ms of that and
578
+ ajv 19.7 ms, ata 2.75 ms, no build step required.** ata registers in 0.25 ms of that and
579
579
  compiles on the first request, so counting only registration would overstate the gap.
580
580
  Reproduce with `node benchmark/bench_fastify_boot.mjs`.
581
581
 
package/bin/ata.js CHANGED
@@ -11,6 +11,7 @@ Usage:
11
11
  ata compile <schema-file> [options] Compile one schema to a standalone module.
12
12
  ata build <glob>... [options] Compile a project's schemas (glob pattern) per file.
13
13
  ata validate <schema> <data> [options] Validate a JSON data file against a schema.
14
+ ata migrate [dir] [options] Report what switching from ajv would change; --write applies it.
14
15
 
15
16
  Validate options:
16
17
  --pretty Render errors with source frames (default on TTY)
@@ -29,6 +30,11 @@ Compile options:
29
30
  --source Embed schema source map (default in development)
30
31
  --no-source Omit source map (default in production, NODE_ENV=production)
31
32
 
33
+ Migrate options:
34
+ --write Apply the mechanical changes (import specifiers, addFormats calls).
35
+ Without it the command only reports. Exit code 1 when a site
36
+ needs a reader, so it can run as a readiness check in CI.
37
+
32
38
  Build options:
33
39
  --out-dir <dir> Write outputs into this directory instead of alongside sources
34
40
  --suffix <str> Output filename suffix (default: ".compiled")
@@ -98,6 +104,7 @@ function parseArgs(argv) {
98
104
  continue;
99
105
  }
100
106
  if (a === '--watch') { out.opts.watch = true; continue; }
107
+ if (a === '--write') { out.opts.write = true; continue; }
101
108
  if (a === '--source') { out.opts.source = true; continue; }
102
109
  if (a === '--no-source') { out.opts.source = false; continue; }
103
110
  if (a === '--dual') { out.opts.dual = true; continue; }
@@ -451,6 +458,15 @@ function main() {
451
458
  return;
452
459
  }
453
460
 
461
+ if (cmd === 'migrate') {
462
+ const { migrate, formatReport } = require('../lib/migrate');
463
+ const result = migrate(args._[0] || '.', { write: !!args.opts.write });
464
+ process.stdout.write(formatReport(result, { write: !!args.opts.write }) + '\n');
465
+ // Non-zero when something needs a reader, so the command works as a
466
+ // readiness check in CI.
467
+ process.exit(result.flagged > 0 ? 1 : 0);
468
+ }
469
+
454
470
  process.stderr.write(`error: unknown command "${cmd}"\n\n`);
455
471
  usage();
456
472
  process.exit(1);
package/index.js CHANGED
@@ -243,14 +243,15 @@ function installCodegenPaths (ctx) {
243
243
  this.validate = preprocess
244
244
  ? (data) => { preprocess(data); return _fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
245
245
  : (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
246
- } else if (hasDynRef && _isCodegen && jsFn) {
247
- // $dynamicRef with JS codegen: direct path, no wrapper layers
248
- const _fn = jsFn, _efn = errOnly, _R = VALID_RESULT;
249
- this.validate = preprocess
250
- ? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
251
- : (data) => _fn(data) ? _R : _efn(data);
252
- } else if (hasDynRef) {
253
- // $dynamicRef without codegen: the interpreted engine. It scores the
246
+ } else if (hasDynRef && !(_isCodegen && jsFn)) {
247
+ // $dynamicRef without codegen: the interpreted engine. A schema the
248
+ // generators unroll by scope (expandDynamicScopes) takes the assembly
249
+ // below like any other, with the one-pass function once its errors are
250
+ // read; a direct verdict-then-error-function path kept for these from
251
+ // before the unrolling cost the suite's dynamicRef groups 5229 ns a pass
252
+ // where the assembly answers in 3136. (A first attempt dropped them into
253
+ // this interpreter branch instead and measured slower; the condition
254
+ // here is what makes them fall through.) It scores the
254
255
  // same on the suite's $dynamicRef cases as the native walker since the
255
256
  // dynamic-scope fix, needs no addon, and gets the verdict-only mode.
256
257
  if (!_interp) {
@@ -545,7 +546,8 @@ installCodegenPaths.compileVerdict = function compileVerdict () {
545
546
  // validateJSON and isValidJSON over a schema-directed scanner, which answers
546
547
  // from the JSON text without building the document.
547
548
  installCodegenPaths.installScanner = function installScanner (schemaObj, options) {
548
- const { ABORT_EARLY_RESULT, VALID_RESULT, _bindEntry } = core._internals;
549
+ const { ABORT_EARLY_RESULT, VALID_RESULT, _bindEntry, _jsonSyntaxRejection } = core._internals;
550
+ let textRejects = 0;
549
551
  const self = this;
550
552
  // Generating a scanner costs about 20 microseconds, measured, and it
551
553
  // saves from around 85 nanoseconds on a small accepted document to
@@ -619,16 +621,58 @@ installCodegenPaths.installScanner = function installScanner (schemaObj, options
619
621
  {
620
622
  const validateByParsing = this.validateJSON;
621
623
  const abortEarly = !!options.abortEarly;
624
+ // A validator whose errors are being read answers validate() with
625
+ // the one-pass function (the switch in lib/validator-core.js), and
626
+ // without richErrors nothing it hands out depends on the text. A
627
+ // rejected text is then parsed and given to that function directly,
628
+ // instead of to the layers below, which parsed it, decided again and
629
+ // wrapped a rejection whose errors are built on first read: a 1.7 KB
630
+ // rejected body took 9.2 microseconds through those layers.
631
+ // The scanner itself is the other half of that cost on a rejected
632
+ // text (2.7 of the 9.2 on that body) and all the saving on an accepted
633
+ // one, which it answers without parsing. So the scan runs while the
634
+ // texts a validator sees are mostly accepted, and is skipped while
635
+ // they are mostly rejected: `bias` counts rejections against
636
+ // acceptances, from either path, and the mode follows its sign with
637
+ // some room, so a mixed stream does not flip on every document.
638
+ const onePassText = (text) => {
639
+ let op = self._onePass;
640
+ // A validator rejecting text only never reads through the object
641
+ // path's switch; the second rejected text makes it.
642
+ if (op === undefined && self._tierOnePass !== undefined && ++textRejects >= 2) { op = self._tierOnePass() || undefined; if (op === undefined) self._tierOnePass = undefined; }
643
+ if (op === undefined) return undefined;
644
+ let obj;
645
+ try { obj = JSON.parse(text); } catch (e) { if (!(e instanceof SyntaxError)) throw e; return _jsonSyntaxRejection(e); }
646
+ return op(obj);
647
+ };
622
648
  this.validateJSON = (jsonStr) => {
623
649
  const scan = self._ensureScanner();
624
650
  if (scan === undefined) return validateByParsing(jsonStr);
625
- if (scan === null) { _bindEntry(self, 'validateJSON', validateByParsing); return validateByParsing(jsonStr); }
651
+ if (scan === null) {
652
+ // No scanner for this schema: the one-pass function still answers a
653
+ // lean validator's texts directly once it is installed.
654
+ _bindEntry(self, 'validateJSON', self._richErrors ? validateByParsing : (text) => {
655
+ if (typeof text === 'string') { const o = onePassText(text); if (o !== undefined) return o; }
656
+ return validateByParsing(text);
657
+ });
658
+ return self.validateJSON(jsonStr);
659
+ }
660
+ let bias = 0;
626
661
  _bindEntry(self, 'validateJSON', (text) => {
627
662
  if (typeof text === 'string') {
663
+ if (bias > 16 && !self._richErrors && self._onePass !== undefined) {
664
+ const r = onePassText(text);
665
+ if (r !== undefined) { if (r.valid) { if (--bias < -32) bias = -32; } else if (bias < 64) bias++; return r; }
666
+ }
628
667
  const r = scan(text);
629
- if (r === 1) return VALID_RESULT;
668
+ if (r === 1) { if (--bias < -32) bias = -32; return VALID_RESULT; }
630
669
  if (r === 0) {
670
+ if (bias < 64) bias++;
631
671
  if (abortEarly) return ABORT_EARLY_RESULT;
672
+ if (!self._richErrors) {
673
+ const o = onePassText(text);
674
+ if (o !== undefined) return o;
675
+ }
632
676
  self._skipNativeFast = true;
633
677
  try {
634
678
  return validateByParsing(text);
@@ -637,6 +681,9 @@ installCodegenPaths.installScanner = function installScanner (schemaObj, options
637
681
  }
638
682
  }
639
683
  }
684
+ // The scanner could not decide (a shape it does not read): the
685
+ // one-pass function answers a lean validator here too.
686
+ if (!self._richErrors && typeof text === 'string') { const o = onePassText(text); if (o !== undefined) return o; }
640
687
  return validateByParsing(text);
641
688
  });
642
689
  return self.validateJSON(jsonStr);
@@ -870,7 +917,21 @@ function buildParse (self, decline, extended) {
870
917
  if (self._mutatesInput) {
871
918
  try { target = structuredClone(data); } catch { target = data; }
872
919
  }
873
- e.errors = self.validate(target).errors;
920
+ // The errors are worked out when first read, as validate() works them
921
+ // out: building every error with its enrichment before throwing made a
922
+ // rejected parse() cost about 12 µs on a document with 16 violations,
923
+ // most of it for callers that only catch. Read once, the list stays.
924
+ const rejection = self.validate(target);
925
+ Object.defineProperty(e, 'errors', {
926
+ configurable: true,
927
+ enumerable: true,
928
+ get () {
929
+ const errors = rejection.errors;
930
+ Object.defineProperty(e, 'errors', { value: errors, writable: true, configurable: true, enumerable: true });
931
+ return errors;
932
+ },
933
+ set (v) { Object.defineProperty(e, 'errors', { value: v, writable: true, configurable: true, enumerable: true }); },
934
+ });
874
935
  throw e;
875
936
  }
876
937
  return copy(data);
@@ -112,4 +112,4 @@ function embedSource () {
112
112
  'const __ataCollapse=' + __ataCollapse.toString() + ';';
113
113
  }
114
114
 
115
- module.exports = { collapseBranches, scoreBranch: __ataScore, SEVERITY: __ATA_SEVERITY, embedSource };
115
+ module.exports = { collapseBranches, scoreBranch: __ataScore, SEVERITY: __ATA_SEVERITY, embedSource, _collapse: __ataCollapse, _multi: __ataMulti };
@@ -0,0 +1,149 @@
1
+ 'use strict'
2
+
3
+ // What the one-pass (combined) function needs at run time and does not need
4
+ // a copy of: one module-level implementation each, handed to every compiled
5
+ // program as a closure value. Each program used to carry its own copy of
6
+ // these in its source, so a thousand validators meant a thousand `_ap`s, a
7
+ // thousand `__ataCollapse`s and so on, each parsed, each compiled by the
8
+ // baseline compiler, each queued for the optimizing compiler on its own.
9
+ // On the official suite, with about a thousand validators in one process,
10
+ // the optimizing compiler got to 728 functions in five seconds, and the
11
+ // validators themselves (`_vC`) were 242 of those. Shared, a helper is
12
+ // compiled once and is hot from the second validator on. The generated
13
+ // standalone modules (lib/aot-impl.js) are not built from this generator and
14
+ // keep their own embedded copies.
15
+
16
+ // The error list's order, kept as it grows (see apOrd in lib/js-compiler.js):
17
+ // `os` the ordinal of each error in push order, `mo` whether they are still in
18
+ // order, `lo` the last ordinal. One state per compiled program, reset at the
19
+ // top of each call.
20
+ function apState () { return { mo: true, lo: -1, on: 0, os: [] } }
21
+
22
+ // Whether a collapsed error holds, at any depth, a branch list of more than
23
+ // one error, which only the reader puts in schema order.
24
+ function multiBranch (x) {
25
+ for (let b = x.branchErrors; b !== undefined; b = b[0].branchErrors) {
26
+ if (b.length > 1) return true
27
+ if (b.length === 0) return false
28
+ }
29
+ return false
30
+ }
31
+
32
+ // Append one error, keeping whether the list is still in the reader's order.
33
+ function ap (S, e, x, o) {
34
+ // Keys are dense integers in the reader's order (ordinal, then the rank of
35
+ // the path), given per program when it is assembled (assignOrdKeys in
36
+ // lib/js-compiler.js): two errors with one exact key are ones the reader
37
+ // keeps as they came. -1 is a key the program does not know. Below -1 is a
38
+ // coarse key, -(k+2): every error behind a reference into a shared
39
+ // definition takes the key k of the reference that reached it, so two of
40
+ // them with one key and different paths are ordered by the reader.
41
+ if (o < 0) {
42
+ if (o === -1) S.mo = false
43
+ else {
44
+ const k = -o - 2
45
+ if (k < S.lo || (k === S.lo && (e === undefined || x.schemaPath !== e[e.length - 1].schemaPath))) S.mo = false
46
+ else S.lo = k
47
+ }
48
+ } else if (o < S.lo) S.mo = false
49
+ else S.lo = o
50
+ // A collapsed branch error's own lists, at any depth, are ordered by the
51
+ // reader (sr hands such a list to it); an in-order outer list must still
52
+ // get there.
53
+ if (x.branchErrors !== undefined && multiBranch(x)) S.mo = false
54
+ S.os[S.on++] = o
55
+ if (e === undefined) return [x]
56
+ e.push(x)
57
+ return e
58
+ }
59
+
60
+ // Put a list that did not come in order into order by the ordinals collected
61
+ // beside it: a stable insertion sort over a few integers. The reader's sort
62
+ // (`srt`) still answers where a key is unknown (-1), where two errors share a
63
+ // coarse key but not a path, where a collapsed error holds a branch list of
64
+ // more than one error, and for long lists.
65
+ function sr (S, e, srt) {
66
+ const n = e.length
67
+ const os = S.os
68
+ if (n > 16 || n !== S.on) return srt(e)
69
+ let coarse = 0
70
+ for (let i = 0; i < n; i++) {
71
+ let k = os[i]
72
+ if (k < 0) {
73
+ if (k === -1) return srt(e)
74
+ k = -k - 2
75
+ os[i] = k
76
+ coarse |= 1 << i
77
+ }
78
+ // A collapsed branch error carries the closest branch's errors, which
79
+ // the reader puts in schema order too; one error needs no ordering.
80
+ if (e[i].branchErrors !== undefined && multiBranch(e[i])) return srt(e)
81
+ }
82
+ // Two errors with one key and different paths, one of them coarse: the
83
+ // reader ranks their paths.
84
+ if (coarse !== 0) {
85
+ for (let i = 1; i < n; i++) {
86
+ for (let j = 0; j < i; j++) if (os[j] === os[i] && ((coarse >> i | coarse >> j) & 1) && e[j].schemaPath !== e[i].schemaPath) return srt(e)
87
+ }
88
+ }
89
+ for (let i = 1; i < n; i++) {
90
+ const k = os[i]
91
+ const x = e[i]
92
+ let j = i - 1
93
+ while (j >= 0 && os[j] > k) { os[j + 1] = os[j]; e[j + 1] = e[j]; j-- }
94
+ os[j + 1] = k
95
+ e[j + 1] = x
96
+ }
97
+ return e
98
+ }
99
+
100
+ // `sr` checked against the reader's order on every list; the test suites run
101
+ // the generator with ATA_CHECK_SORT set, which routes here.
102
+ function srX (S, e, srt) {
103
+ const c = e.slice()
104
+ const k = S.os.slice(0, S.on)
105
+ const a = sr(S, e, srt)
106
+ const b = srt(c)
107
+ if (a.length !== b.length || a.some((x, i) => x.schemaPath !== b[i].schemaPath || x.instancePath !== b[i].instancePath)) {
108
+ throw new Error('ata: local error sort disagrees with the reader ' + JSON.stringify(a.map((x) => x.schemaPath)) + ' vs ' + JSON.stringify(b.map((x) => x.schemaPath)) + ' keys ' + JSON.stringify(k))
109
+ }
110
+ return a
111
+ }
112
+
113
+ // Presence helpers (see OWN_HELPER_CODE in lib/js-compiler.js for the text
114
+ // the standalone modules carry). The prototype test itself stays inline in
115
+ // the generated code, read once per object: a shared helper reading the
116
+ // prototype of every object it was asked about was megamorphic.
117
+ const _PN = ({}).__proto__ === Object.prototype // eslint-disable-line no-proto
118
+ const _OP = Object.prototype
119
+ const _gp = Object.getPrototypeOf
120
+ const _hop = Object.prototype.hasOwnProperty
121
+ function _h (o, k) { return _hop.call(o, k) }
122
+ function _ok (o, k) { return (_PN ? o.__proto__ : _gp(o)) === _OP || _hop.call(o, k) } // eslint-disable-line no-proto
123
+ function _hall (o, ks) { for (let i = 0; i < ks.length; i++) if (!_hop.call(o, ks[i])) return false; return true }
124
+
125
+ // RFC 6901 escaping for a key only known at run time (the static ones are
126
+ // escaped when the code is written).
127
+ function _pe (s) {
128
+ for (let i = 0; i < s.length; i++) {
129
+ const c = s.charCodeAt(i)
130
+ if (c === 126 || c === 47) return s.replace(/~/g, '~0').replace(/\//g, '~1')
131
+ }
132
+ return s
133
+ }
134
+
135
+ // The annotation functions' constants (emitAnnot): the boolean schemas, and
136
+ // the rollback of the evaluated-name list when a branch fails.
137
+ function _anT () { return true }
138
+ function _anF () { return false }
139
+ function _rb (S, m) { if (S.length !== m) S.length = m }
140
+
141
+ module.exports = {
142
+ apState, ap, sr, srX,
143
+ _PN, _OP, _gp, _hop, _h, _ok, _hall, _pe, _anT, _anF, _rb,
144
+ }
145
+ // Loaded when a program first asks for them: a schema without const, enum,
146
+ // uniqueItems, oneOf or anyOf never needs these modules on its cold path.
147
+ Object.defineProperty(module.exports, '_deq', { enumerable: true, get () { return require('./unique-items')._deq } })
148
+ Object.defineProperty(module.exports, '__ataCollapse', { enumerable: true, get () { return require('./branch-collapse')._collapse } })
149
+ Object.defineProperty(module.exports, '__ataMulti', { enumerable: true, get () { return require('./branch-collapse')._multi } })
package/lib/formats.js CHANGED
@@ -382,12 +382,18 @@ function uriChar (c) {
382
382
  function isHex (c) {
383
383
  return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70);
384
384
  }
385
+ // The same set through the URI_CHAR and HEX_CHAR tables: one load per
386
+ // character, where uriChar() was a call and up to eleven comparisons, and on
387
+ // SchemaStore's SARIF samples, full of URIs, that loop was a sixth of what the
388
+ // interpreted engine spent. A "%" at the end reads past the string, NaN,
389
+ // which fails the bound as it failed isHex().
385
390
  function uriChars (s, from) {
386
391
  for (let i = from; i < s.length; i++) {
387
392
  const c = s.charCodeAt(i);
388
- if (!uriChar(c)) return false;
393
+ if (c > 127 || URI_CHAR[c] === 0) return false;
389
394
  if (c === 37) { // '%'
390
- if (!isHex(s.charCodeAt(i + 1)) || !isHex(s.charCodeAt(i + 2))) return false;
395
+ const h1 = s.charCodeAt(i + 1), h2 = s.charCodeAt(i + 2);
396
+ if (!(h1 <= 127) || !(h2 <= 127) || HEX_CHAR[h1] === 0 || HEX_CHAR[h2] === 0) return false;
391
397
  i += 2;
392
398
  }
393
399
  }
@@ -183,26 +183,22 @@ function resolveRef(ref, fromBase, state) {
183
183
 
184
184
  // ---------- value helpers ----------
185
185
 
186
- function deepEqual(a, b) {
187
- if (a === b) return true;
188
- if (typeof a === 'number' && typeof b === 'number') return a === b;
189
- if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
190
- const aArr = Array.isArray(a);
191
- if (aArr !== Array.isArray(b)) return false;
192
- if (aArr) {
193
- if (a.length !== b.length) return false;
194
- for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
195
- return true;
196
- }
197
- const ak = Object.keys(a);
198
- const bk = Object.keys(b);
199
- if (ak.length !== bk.length) return false;
200
- for (const k of ak) {
201
- if (!(k in b) || !deepEqual(a[k], b[k])) return false;
202
- }
203
- return true;
186
+ // uniqueItems: the functions the code generator emits as _uq and _uqp
187
+ // (lib/unique-items.js), so both engines run the same code.
188
+ const { allDistinct, duplicatePair, _deq } = require('./unique-items');
189
+
190
+ // Whether `data` deep-equals one of the node's enum values (see enumPrims).
191
+ function enumMember(P, data) {
192
+ if (data === null || typeof data !== 'object') return P.enumPrims.has(data);
193
+ const objs = P.enumObjs;
194
+ for (let i = 0; i < objs.length; i++) if (deepEqual(objs[i], data)) return true;
195
+ return false;
204
196
  }
205
197
 
198
+ // JSON equality: the same function the code generator emits as `_deq`, so the
199
+ // engines agree on what equal means (an object's own keys, in any order).
200
+ const deepEqual = _deq;
201
+
206
202
  function codePointLength(s) {
207
203
  let n = 0;
208
204
  for (let i = 0; i < s.length; i++) {
@@ -375,6 +371,21 @@ class Plan {
375
371
  }
376
372
  this.hasType = schema.type !== undefined;
377
373
  this.enum = schema.enum !== undefined ? schema.enum : null;
374
+ // Membership for enumMember(): the primitive values in a Set, so a long
375
+ // enum answers in one lookup, and the object and array values, which only
376
+ // deepEqual can compare, in a list. Scanning every value with deepEqual
377
+ // was a fifth to a third of what validating SchemaStore's samples cost.
378
+ // NaN stays out of the Set: deepEqual never matches it, and a Set would.
379
+ this.enumPrims = null;
380
+ this.enumObjs = null;
381
+ if (Array.isArray(this.enum)) {
382
+ this.enumPrims = new Set();
383
+ this.enumObjs = [];
384
+ for (const v of this.enum) {
385
+ if (v !== null && typeof v === 'object') this.enumObjs.push(v);
386
+ else if (!(typeof v === 'number' && v !== v)) this.enumPrims.add(v);
387
+ }
388
+ }
378
389
  this.hasConst = schema.const !== undefined;
379
390
  this.const = schema.const;
380
391
 
@@ -524,9 +535,7 @@ function evalLeafV(P, data) {
524
535
  const bits = dataBits(data);
525
536
  if (P.hasType && (bits & P.typeMask) === 0) return false;
526
537
  if (P.enum !== null) {
527
- let found = false;
528
- for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
529
- if (!found) return false;
538
+ if (!enumMember(P, data)) return false;
530
539
  }
531
540
  if (P.hasConst && !deepEqual(P.const, data)) return false;
532
541
  if (P.hasNumber && Number.isFinite(data)) {
@@ -545,13 +554,7 @@ function evalLeafV(P, data) {
545
554
  if (P.hasArray && bits === T_ARRAY) {
546
555
  if (P.minItems !== undefined && data.length < P.minItems) return false;
547
556
  if (P.maxItems !== undefined && data.length > P.maxItems) return false;
548
- if (P.uniqueItems) {
549
- for (let i = 0; i < data.length; i++) {
550
- for (let j = i + 1; j < data.length; j++) {
551
- if (deepEqual(data[i], data[j])) return false;
552
- }
553
- }
554
- }
557
+ if (P.uniqueItems && !allDistinct(data)) return false;
555
558
  }
556
559
  if (P.hasObject && bits === T_OBJECT) {
557
560
  if (P.required !== null) {
@@ -635,9 +638,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
635
638
  valid = false;
636
639
  }
637
640
  if (P.enum !== null) {
638
- let found = false;
639
- for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
640
- if (!found) {
641
+ if (!enumMember(P, data)) {
641
642
  if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
642
643
  valid = false;
643
644
  }
@@ -663,14 +664,11 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
663
664
  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; }
664
665
  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; }
665
666
  if (P.uniqueItems) {
666
- outer: for (let i = 0; i < data.length; i++) {
667
- for (let j = i + 1; j < data.length; j++) {
668
- if (deepEqual(data[i], data[j])) {
669
- if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
670
- valid = false;
671
- break outer;
672
- }
673
- }
667
+ const pair = duplicatePair(data);
668
+ if (pair !== null) {
669
+ const i = pair[0], j = pair[1];
670
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
671
+ valid = false;
674
672
  }
675
673
  }
676
674
  }
@@ -924,9 +922,7 @@ class Interpreter {
924
922
  valid = false;
925
923
  }
926
924
  if (P.enum !== null) {
927
- let found = false;
928
- for (let i = 0; i < P.enum.length; i++) { if (deepEqual(P.enum[i], data)) { found = true; break; } }
929
- if (!found) {
925
+ if (!enumMember(P, data)) {
930
926
  if (errors !== NOERRORS) errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: P.enum }, 'must be equal to one of the allowed values'));
931
927
  valid = false;
932
928
  }
@@ -958,14 +954,11 @@ class Interpreter {
958
954
  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; }
959
955
  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; }
960
956
  if (P.uniqueItems) {
961
- outer: for (let i = 0; i < data.length; i++) {
962
- for (let j = i + 1; j < data.length; j++) {
963
- if (deepEqual(data[i], data[j])) {
964
- if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
965
- valid = false;
966
- break outer;
967
- }
968
- }
957
+ const pair = duplicatePair(data);
958
+ if (pair !== null) {
959
+ const i = pair[0], j = pair[1];
960
+ if (errors !== NOERRORS) errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, `must NOT have duplicate items (items ## ${j} and ${i} are identical)`));
961
+ valid = false;
969
962
  }
970
963
  }
971
964
  const prefix = P.prefixItems;
@@ -1246,7 +1239,7 @@ function err(code, keyword, instancePath, schemaPath, params, message) {
1246
1239
 
1247
1240
  const { compileInterpreter } = require('./plan-compiler').install({
1248
1241
  Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
1249
- deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
1242
+ deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
1250
1243
  T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
1251
1244
  });
1252
1245
 
@@ -1258,7 +1251,7 @@ module.exports = { createInterpreter };
1258
1251
  // The resolver, for the code generator: it resolves references at compile
1259
1252
  // time the way this engine does at run time (normalizeRefs in js-compiler).
1260
1253
  Object.defineProperty(module.exports, '_refInternals', {
1261
- value: { indexSchemas, resolveRef, FALLBACK_BASE },
1254
+ value: { indexSchemas, resolveRef, splitFragment, resolveUri, FALLBACK_BASE },
1262
1255
  enumerable: false,
1263
1256
  });
1264
1257
  // For lib/plan-source.js, which emits source from the same plans and must call