ata-validator 1.44.0 → 1.45.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 +9 -9
- package/lib/formats.js +8 -2
- package/lib/interpreter.js +44 -51
- package/lib/js-compiler.js +46 -25
- package/lib/plan-compiler.js +23 -25
- package/lib/unique-items.js +139 -0
- package/lib/version.js +1 -1
- package/package.json +9 -9
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** |
|
|
248
|
-
| Time to a served request | **3.5 ms** | 8.
|
|
247
|
+
| In a bundle, gzipped | **2.1 KB** | 112.8 KB |
|
|
248
|
+
| Time to a served request | **3.5 ms** | 8.6 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.
|
|
253
|
+
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.44.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.
|
|
255
|
+
`benchmark/bundle`, against 3.5 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.**
|
|
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
|
|
|
@@ -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
|
|
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 112.8 KB for the runtime bundled for the browser (ata 1.44.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.
|
|
536
|
+
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.44.0:
|
|
537
537
|
|
|
538
538
|
| What the app imports | Size | Notes |
|
|
539
539
|
|---|---|---|
|
|
540
|
-
| `Validator` from `ata-validator` |
|
|
540
|
+
| `Validator` from `ata-validator` | 112.8 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.
|
|
578
|
+
ajv 19.4 ms, ata 2.7 ms, no build step required.** ata registers in 0.2 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/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 (
|
|
393
|
+
if (c > 127 || URI_CHAR[c] === 0) return false;
|
|
389
394
|
if (c === 37) { // '%'
|
|
390
|
-
|
|
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
|
}
|
package/lib/interpreter.js
CHANGED
|
@@ -183,26 +183,22 @@ function resolveRef(ref, fromBase, state) {
|
|
|
183
183
|
|
|
184
184
|
// ---------- value helpers ----------
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
|
package/lib/js-compiler.js
CHANGED
|
@@ -10,12 +10,20 @@
|
|
|
10
10
|
// forms answers a different question and costs a string per call.
|
|
11
11
|
const DEQ_HELPER = "function _deq(a,b){if(a===b)return true;if(a===null||b===null||typeof a!=='object'||typeof b!=='object')return false;var aa=Array.isArray(a);if(aa!==Array.isArray(b))return false;var i;if(aa){if(a.length!==b.length)return false;for(i=0;i<a.length;i++)if(!_deq(a[i],b[i]))return false;return true}var ka=Object.keys(a);if(ka.length!==Object.keys(b).length)return false;for(i=0;i<ka.length;i++){var k=ka[i];if(!Object.prototype.hasOwnProperty.call(b,k)||!_deq(a[k],b[k]))return false}return true}"
|
|
12
12
|
|
|
13
|
-
// uniqueItems helpers
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// uniqueItems helpers, emitted from the functions the interpreted engine
|
|
14
|
+
// calls (lib/unique-items.js): `_uq` answers whether every item is distinct.
|
|
15
|
+
// Loaded at the first schema with uniqueItems, not on every cold start.
|
|
16
|
+
// Comment lines and indentation are dropped: the text lands in every
|
|
17
|
+
// generated function and ahead-of-time module that checks uniqueItems.
|
|
18
|
+
let _uqSources = null
|
|
19
|
+
function uqSources () {
|
|
20
|
+
if (_uqSources === null) {
|
|
21
|
+
const u = require('./unique-items')
|
|
22
|
+
const compact = (fn) => String(fn).replace(/\n\s*\/\/[^\n]*/g, '').replace(/\n\s*/g, '\n')
|
|
23
|
+
_uqSources = { uq: compact(u._uqh) + compact(u._uq), uqp: 'var _uqpI=0;' + compact(u._uqp) }
|
|
24
|
+
}
|
|
25
|
+
return _uqSources
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
// Where a generator hoists a declaration so it runs once per compiled
|
|
21
29
|
// function rather than once per call. The boolean generator has a preamble
|
|
@@ -97,7 +105,7 @@ function emitPtrEsc(ctx) {
|
|
|
97
105
|
// `_uq` compares items with `_deq`, so it can never be hoisted alone.
|
|
98
106
|
function emitUq(ctx) {
|
|
99
107
|
emitDeq(ctx)
|
|
100
|
-
hoistOnce(ctx, '_uqHoisted',
|
|
108
|
+
hoistOnce(ctx, '_uqHoisted', uqSources().uq)
|
|
101
109
|
return '_uq'
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -107,15 +115,11 @@ function emitUq(ctx) {
|
|
|
107
115
|
// a later equal, paired with its first later equal. That is the pair the
|
|
108
116
|
// interpreted engine reports, and the generators used to report a different
|
|
109
117
|
// one (the earliest repeat, paired with what it repeats), so the same document
|
|
110
|
-
// named a different pair depending on the engine that answered.
|
|
111
|
-
//
|
|
112
|
-
// canonical string once an object appears. Building a canonical string for
|
|
113
|
-
// every item on every call, as the generators did, made reading a uniqueItems
|
|
114
|
-
// error up to ten times the cost of the verdict.
|
|
115
|
-
const UQP_HELPER = "var _uqpI=0;function _uqp(a){var n=a.length,i,k;if(n<2)return -1;if(n<=12){for(i=0;i<n-1;i++)for(k=i+1;k<n;k++)if(_deq(a[i],a[k])){_uqpI=i;return k}return -1}var prim=true;for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object'){prim=false;break}}var m=new Map(),bi=-1,bj=-1;for(i=0;i<n;i++){var c=prim?a[i]:_cn(a[i]);var p=m.get(c);if(p===undefined)m.set(c,i);else if(p>=0){if(bi<0||p<bi){bi=p;bj=i}m.set(c,-1)}}if(bi<0)return -1;_uqpI=bi;return bj}"
|
|
118
|
+
// named a different pair depending on the engine that answered. See
|
|
119
|
+
// lib/unique-items.js for how it finds the pair.
|
|
116
120
|
function emitUqp(ctx) {
|
|
117
121
|
emitUq(ctx)
|
|
118
|
-
hoistOnce(ctx, '_uqpHoisted',
|
|
122
|
+
hoistOnce(ctx, '_uqpHoisted', uqSources().uqp)
|
|
119
123
|
return '_uqp'
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -3481,6 +3485,10 @@ function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, ty
|
|
|
3481
3485
|
// up. The previous form built the canonical closure and a Set on
|
|
3482
3486
|
// every call, even for [1, 2, 3].
|
|
3483
3487
|
inner = `if(!${emitUq(ctx)}(${v}))return false`
|
|
3488
|
+
} else if (ctx.helperCode) {
|
|
3489
|
+
// Inside the error or combined function, whose helper code is emitted
|
|
3490
|
+
// into its source, the same hoisted helper.
|
|
3491
|
+
inner = `if(!${emitUq(ctx)}(${v}))return false`
|
|
3484
3492
|
} else {
|
|
3485
3493
|
inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Set();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);if(_s${si}.has(_k))return false;_s${si}.add(_k)}`
|
|
3486
3494
|
}
|
|
@@ -5203,7 +5211,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
5203
5211
|
// In the combined function `_e` is created on the first error and every
|
|
5204
5212
|
// error is collected (`_all` is true there), so the report only pushes.
|
|
5205
5213
|
const report = ctx.inCombined && !ctx.inBranchFn
|
|
5206
|
-
? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e
|
|
5214
|
+
? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e=_ap(_e,${collapsed}))`
|
|
5207
5215
|
: `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
|
|
5208
5216
|
// The node's runtime unevaluatedProperties already counted how many of
|
|
5209
5217
|
// these branches pass, through the annotation functions, which answer the
|
|
@@ -5219,7 +5227,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
5219
5227
|
// branch errors, so it is built from the bits the count left.
|
|
5220
5228
|
if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
|
|
5221
5229
|
const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
|
|
5222
|
-
guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e
|
|
5230
|
+
guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e=_ap(_e,__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0})))}else if(!(${same}&&${counted.count}===1))`
|
|
5223
5231
|
}
|
|
5224
5232
|
// Without such a count, each branch also gets a verdict function, and the
|
|
5225
5233
|
// passing branches are counted with those: a oneOf with exactly one and an
|
|
@@ -5280,7 +5288,7 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
5280
5288
|
? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
|
|
5281
5289
|
: ''
|
|
5282
5290
|
const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
|
|
5283
|
-
const pushMulti = top ? `(_e
|
|
5291
|
+
const pushMulti = top ? `(_e=_ap(_e,${multi}))` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
|
|
5284
5292
|
const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
|
|
5285
5293
|
lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}}}}`)
|
|
5286
5294
|
}
|
|
@@ -6012,7 +6020,7 @@ function sharedErr (lit, rootSchema) {
|
|
|
6012
6020
|
// the document. `paramsShared` names a frozen params object hoisted for a
|
|
6013
6021
|
// static site, so a static error's params stay one shared object as before.
|
|
6014
6022
|
function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
|
|
6015
|
-
if (!ctx.rich) return `(_e
|
|
6023
|
+
if (!ctx.rich) return `(_e=_ap(_e,${lean}))`
|
|
6016
6024
|
if (!ctx.richMk) {
|
|
6017
6025
|
ctx.richMk = true
|
|
6018
6026
|
const { makeRich } = require('./enrich-site')
|
|
@@ -6028,7 +6036,7 @@ function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, le
|
|
|
6028
6036
|
// the definition and the reference's own comes in as `_sp`.
|
|
6029
6037
|
const rel = sp.startsWith(SP_PLACEHOLDER)
|
|
6030
6038
|
ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
|
|
6031
|
-
return `(_e
|
|
6039
|
+
return `(_e=_ap(_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''})))`
|
|
6032
6040
|
}
|
|
6033
6041
|
|
|
6034
6042
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
|
|
@@ -6110,7 +6118,11 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
6110
6118
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
6111
6119
|
|
|
6112
6120
|
// runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
|
|
6113
|
-
|
|
6121
|
+
// `_ap` adds an error to a list that may not exist yet: the first one
|
|
6122
|
+
// creates it as a one-element literal. Pushing onto an empty `[]` grew its
|
|
6123
|
+
// backing store on every rejection, about 7% of reading errors across the
|
|
6124
|
+
// official suite.
|
|
6125
|
+
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: ['function _ap(e,x){if(e===undefined)return[x];e.push(x);return e}'], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
6114
6126
|
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
|
|
6115
6127
|
const lines = []
|
|
6116
6128
|
try {
|
|
@@ -6273,6 +6285,14 @@ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix)
|
|
|
6273
6285
|
ctx.defFnsC.set(key, fnName)
|
|
6274
6286
|
const outerHoisted = ctx.hoisted
|
|
6275
6287
|
ctx.hoisted = []
|
|
6288
|
+
// The body is a function of its own: a reference the caller was in the
|
|
6289
|
+
// middle of expanding is not on the stack inside it, where reaching it
|
|
6290
|
+
// again is a call (this function, or another one) rather than a loop.
|
|
6291
|
+
// With the caller's stack, a definition expanded once on the way into
|
|
6292
|
+
// the root's function was taken for a cycle inside it, and the schema
|
|
6293
|
+
// (a tree whose nodes hold trees) lost its one-pass function.
|
|
6294
|
+
const outerRefs = ctx.refStack
|
|
6295
|
+
ctx.refStack = new Set()
|
|
6276
6296
|
const bodyLines = []
|
|
6277
6297
|
let hoisted
|
|
6278
6298
|
try {
|
|
@@ -6280,6 +6300,7 @@ function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix)
|
|
|
6280
6300
|
} finally {
|
|
6281
6301
|
hoisted = ctx.hoisted
|
|
6282
6302
|
ctx.hoisted = outerHoisted
|
|
6303
|
+
ctx.refStack = outerRefs
|
|
6283
6304
|
}
|
|
6284
6305
|
let body = bodyLines.join('\n ')
|
|
6285
6306
|
body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
|
|
@@ -6438,12 +6459,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
6438
6459
|
const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
|
|
6439
6460
|
if (o !== null) lit._o = o
|
|
6440
6461
|
ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
|
|
6441
|
-
return `(_e
|
|
6462
|
+
return `(_e=_ap(_e,${errVar}))`
|
|
6442
6463
|
}
|
|
6443
6464
|
}
|
|
6444
6465
|
}
|
|
6445
6466
|
// Dynamic path (e.g., array index): inline as before
|
|
6446
|
-
return `(_e
|
|
6467
|
+
return `(_e=_ap(_e,{keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}}))`
|
|
6447
6468
|
}
|
|
6448
6469
|
|
|
6449
6470
|
// enum and const apply to a value of any type, so a value that fails
|
|
@@ -6533,7 +6554,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
6533
6554
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
6534
6555
|
ctx.closureVars.push(errVar)
|
|
6535
6556
|
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
6536
|
-
lines.push(`if(${check}){(_e
|
|
6557
|
+
lines.push(`if(${check}){(_e=_ap(_e,${errVar}))}`)
|
|
6537
6558
|
} else {
|
|
6538
6559
|
lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
6539
6560
|
}
|
|
@@ -6547,7 +6568,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
6547
6568
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
6548
6569
|
ctx.closureVars.push(errVar)
|
|
6549
6570
|
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
6550
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e
|
|
6571
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_e,${errVar}))}`)
|
|
6551
6572
|
} else {
|
|
6552
6573
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
6553
6574
|
}
|
|
@@ -6699,7 +6720,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
6699
6720
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
6700
6721
|
ctx.closureVars.push(errVar)
|
|
6701
6722
|
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
|
|
6702
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e
|
|
6723
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e=_ap(_e,${errVar}))}`)
|
|
6703
6724
|
} else {
|
|
6704
6725
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
|
|
6705
6726
|
}
|
package/lib/plan-compiler.js
CHANGED
|
@@ -52,12 +52,16 @@ const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string'
|
|
|
52
52
|
|
|
53
53
|
function install(deps) {
|
|
54
54
|
const { Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
55
|
-
deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
|
|
55
|
+
deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
|
|
56
56
|
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
|
|
57
57
|
|
|
58
58
|
const TRUE_FN = () => true;
|
|
59
|
-
|
|
59
|
+
// `leaf`: the node compiled no subschema of its own (see compileNode). A
|
|
60
|
+
// collecting caller asks a leaf for its verdict first and builds the paths
|
|
61
|
+
// for its errors only when it fails.
|
|
62
|
+
const TRUE_PAIR = { v: TRUE_FN, c: TRUE_FN, leaf: true };
|
|
60
63
|
const FALSE_PAIR = {
|
|
64
|
+
leaf: true,
|
|
61
65
|
v: () => false,
|
|
62
66
|
c: (data, errors, instancePath, schemaPath) => {
|
|
63
67
|
if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
@@ -164,12 +168,8 @@ function install(deps) {
|
|
|
164
168
|
checks.push((data, bits) => (bits & mask) !== 0);
|
|
165
169
|
}
|
|
166
170
|
if (P.enum !== null) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
else checks.push((data) => {
|
|
170
|
-
for (let i = 0; i < vals.length; i++) if (deepEqual(vals[i], data)) return true;
|
|
171
|
-
return false;
|
|
172
|
-
});
|
|
171
|
+
if (P.enum.length === 0) checks.push(() => false);
|
|
172
|
+
else checks.push((data) => enumMember(P, data));
|
|
173
173
|
}
|
|
174
174
|
if (P.hasConst) {
|
|
175
175
|
const c = P.const;
|
|
@@ -205,14 +205,7 @@ function install(deps) {
|
|
|
205
205
|
const arrs = [];
|
|
206
206
|
if (P.minItems !== undefined) { const m = P.minItems; arrs.push((d) => d.length >= m); }
|
|
207
207
|
if (P.maxItems !== undefined) { const m = P.maxItems; arrs.push((d) => d.length <= m); }
|
|
208
|
-
if (P.uniqueItems)
|
|
209
|
-
arrs.push((d) => {
|
|
210
|
-
for (let i = 0; i < d.length; i++) {
|
|
211
|
-
for (let j = i + 1; j < d.length; j++) if (deepEqual(d[i], d[j])) return false;
|
|
212
|
-
}
|
|
213
|
-
return true;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
208
|
+
if (P.uniqueItems) arrs.push(allDistinct);
|
|
216
209
|
const run = combineValue(arrs);
|
|
217
210
|
if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_ARRAY || run(data)); }
|
|
218
211
|
}
|
|
@@ -300,6 +293,7 @@ function install(deps) {
|
|
|
300
293
|
const box = { v: null, c: null };
|
|
301
294
|
const pair = {
|
|
302
295
|
open: true,
|
|
296
|
+
leaf: false,
|
|
303
297
|
v: (d, st, rec) => box.v(d, st, rec),
|
|
304
298
|
c: (d, e, ip, sp, st, rec) => box.c(d, e, ip, sp, st, rec),
|
|
305
299
|
};
|
|
@@ -308,8 +302,11 @@ function install(deps) {
|
|
|
308
302
|
// In-place children collect when this node does; child applicators
|
|
309
303
|
// never do. `rec` is threaded as the trailing argument of every step
|
|
310
304
|
// and is undefined on non-collecting nodes, where nothing reads it.
|
|
311
|
-
|
|
312
|
-
|
|
305
|
+
// Whether this node compiles any subschema: a node that does not is a
|
|
306
|
+
// leaf, whose verdict is a few comparisons.
|
|
307
|
+
let nested = false;
|
|
308
|
+
const inPlace = (n, b) => { nested = true; return compileNode(ctx, n, b, chain, collect); };
|
|
309
|
+
const child = (n) => { nested = true; return compileNode(ctx, n, base, chain, false); };
|
|
313
310
|
|
|
314
311
|
const steps = []; // collect variant: (data, errors, ip, sp, stack, rec)
|
|
315
312
|
const vsteps = []; // verdict variant: (data, stack, rec)
|
|
@@ -399,7 +396,7 @@ function install(deps) {
|
|
|
399
396
|
let ok = true;
|
|
400
397
|
const n = Math.min(fns.length, data.length);
|
|
401
398
|
for (let i = 0; i < n; i++) {
|
|
402
|
-
if (!fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack)) {
|
|
399
|
+
if (!((fns[i].leaf && fns[i].v(data[i], stack)) || fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack))) {
|
|
403
400
|
ok = false;
|
|
404
401
|
if (errors === NOERRORS) return false;
|
|
405
402
|
}
|
|
@@ -422,7 +419,7 @@ function install(deps) {
|
|
|
422
419
|
if (dataBits(data) !== T_ARRAY) return true;
|
|
423
420
|
let ok = true;
|
|
424
421
|
for (let i = start; i < data.length; i++) {
|
|
425
|
-
if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack)) {
|
|
422
|
+
if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack))) {
|
|
426
423
|
ok = false;
|
|
427
424
|
if (errors === NOERRORS) return false;
|
|
428
425
|
}
|
|
@@ -536,7 +533,7 @@ function install(deps) {
|
|
|
536
533
|
let evaluated = false;
|
|
537
534
|
const prop = lookup !== null ? lookup(key) : undefined;
|
|
538
535
|
if (prop !== undefined) {
|
|
539
|
-
if (!prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack)) {
|
|
536
|
+
if (!((prop.fn.leaf && prop.fn.v(data[key], stack)) || prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack))) {
|
|
540
537
|
ok = false;
|
|
541
538
|
if (errors === NOERRORS) return false;
|
|
542
539
|
}
|
|
@@ -546,7 +543,7 @@ function install(deps) {
|
|
|
546
543
|
for (let pi = 0; pi < patterns.length; pi++) {
|
|
547
544
|
const pp = patterns[pi];
|
|
548
545
|
if (pp.re.test(key)) {
|
|
549
|
-
if (!pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack)) {
|
|
546
|
+
if (!((pp.fn.leaf && pp.fn.v(data[key], stack)) || pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack))) {
|
|
550
547
|
ok = false;
|
|
551
548
|
if (errors === NOERRORS) return false;
|
|
552
549
|
}
|
|
@@ -559,7 +556,7 @@ function install(deps) {
|
|
|
559
556
|
ok = false;
|
|
560
557
|
if (errors === NOERRORS) return false;
|
|
561
558
|
errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
|
|
562
|
-
} else if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
|
|
559
|
+
} else if (!((apFn.leaf && apFn.v(data[key], stack)) || apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack))) {
|
|
563
560
|
ok = false;
|
|
564
561
|
if (errors === NOERRORS) return false;
|
|
565
562
|
}
|
|
@@ -927,7 +924,7 @@ function install(deps) {
|
|
|
927
924
|
for (let k = 0; k < keys.length; k++) {
|
|
928
925
|
const key = keys[k];
|
|
929
926
|
if (hasProp(rec, key)) continue;
|
|
930
|
-
if (!fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack)) {
|
|
927
|
+
if (!((fn.leaf && fn.v(data[key], stack)) || fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack))) {
|
|
931
928
|
ok = false;
|
|
932
929
|
if (errors === NOERRORS) return false;
|
|
933
930
|
}
|
|
@@ -983,7 +980,7 @@ function install(deps) {
|
|
|
983
980
|
let ok = true;
|
|
984
981
|
for (let i = 0; i < data.length; i++) {
|
|
985
982
|
if (hasItem(rec, i)) continue;
|
|
986
|
-
if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack)) {
|
|
983
|
+
if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack))) {
|
|
987
984
|
ok = false;
|
|
988
985
|
if (errors === NOERRORS) return false;
|
|
989
986
|
}
|
|
@@ -1068,6 +1065,7 @@ function install(deps) {
|
|
|
1068
1065
|
box.v = vfn;
|
|
1069
1066
|
pair.c = cfn;
|
|
1070
1067
|
pair.v = vfn;
|
|
1068
|
+
pair.leaf = !nested;
|
|
1071
1069
|
pair.open = false;
|
|
1072
1070
|
return pair;
|
|
1073
1071
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// uniqueItems for every engine. These are real functions: the interpreted
|
|
4
|
+
// engine calls them, and the code generator emits their source text
|
|
5
|
+
// (`String(fn)`) into the function it builds, so both run the same code.
|
|
6
|
+
// They read only each other, `_deq` and `_uqpI`, which the generator emits
|
|
7
|
+
// beside them.
|
|
8
|
+
//
|
|
9
|
+
// Short arrays compare pairwise and allocate nothing. Longer arrays of
|
|
10
|
+
// primitives key a Set or Map by the value itself. Longer arrays that hold an
|
|
11
|
+
// object key a Map by a numeric structural hash, and only items whose hashes
|
|
12
|
+
// collide are compared with `_deq`. Equal values always hash alike: numbers
|
|
13
|
+
// by value (so 1 and 1.0, 0 and -0 agree), strings by their characters,
|
|
14
|
+
// arrays by position, and objects by the sum of their own key-value hashes,
|
|
15
|
+
// whatever the key order. The hash replaced a sorted-key canonical string
|
|
16
|
+
// built for every item on every call, which was over a quarter of the time a
|
|
17
|
+
// generated verdict spent on SchemaStore's sample documents.
|
|
18
|
+
|
|
19
|
+
// JSON equality, the same function the generator hoists as DEQ_HELPER.
|
|
20
|
+
function _deq(a, b) {
|
|
21
|
+
if (a === b) return true;
|
|
22
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
23
|
+
var aa = Array.isArray(a);
|
|
24
|
+
if (aa !== Array.isArray(b)) return false;
|
|
25
|
+
var i;
|
|
26
|
+
if (aa) {
|
|
27
|
+
if (a.length !== b.length) return false;
|
|
28
|
+
for (i = 0; i < a.length; i++) if (!_deq(a[i], b[i])) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
var ka = Object.keys(a);
|
|
32
|
+
if (ka.length !== Object.keys(b).length) return false;
|
|
33
|
+
for (i = 0; i < ka.length; i++) {
|
|
34
|
+
var k = ka[i];
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(b, k) || !_deq(a[k], b[k])) return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _uqh(x) {
|
|
41
|
+
if (x === null) return 1;
|
|
42
|
+
var t = typeof x, h, i;
|
|
43
|
+
if (t === 'boolean') return x ? 2 : 3;
|
|
44
|
+
if (t === 'number') {
|
|
45
|
+
if (x === 0) return 4;
|
|
46
|
+
h = x | 0;
|
|
47
|
+
return Math.imul(h ^ (((x - h) * 1e9) | 0), 0x9e3779b1) ^ 5;
|
|
48
|
+
}
|
|
49
|
+
if (t === 'string') {
|
|
50
|
+
h = 0x811c9dc5 ^ x.length;
|
|
51
|
+
for (i = 0; i < x.length; i++) h = Math.imul(h ^ x.charCodeAt(i), 16777619);
|
|
52
|
+
return h;
|
|
53
|
+
}
|
|
54
|
+
if (t !== 'object') return 6;
|
|
55
|
+
if (Array.isArray(x)) {
|
|
56
|
+
h = 0x2545f491 ^ x.length;
|
|
57
|
+
for (i = 0; i < x.length; i++) h = Math.imul(h ^ _uqh(x[i]), 16777619);
|
|
58
|
+
return h;
|
|
59
|
+
}
|
|
60
|
+
var ks = Object.keys(x);
|
|
61
|
+
h = 0x6c8e9cf5 ^ ks.length;
|
|
62
|
+
for (i = 0; i < ks.length; i++) h = (h + Math.imul(_uqh(ks[i]) ^ Math.imul(_uqh(x[ks[i]]), 0x5bd1e995), 0x27d4eb2d)) | 0;
|
|
63
|
+
return h;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Whether every item is distinct.
|
|
67
|
+
function _uq(a) {
|
|
68
|
+
var n = a.length, i, k;
|
|
69
|
+
if (n < 2) return true;
|
|
70
|
+
if (n <= 12) {
|
|
71
|
+
for (i = 1; i < n; i++) for (k = 0; k < i; k++) if (_deq(a[i], a[k])) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
var prim = true;
|
|
75
|
+
for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
|
|
76
|
+
if (prim) {
|
|
77
|
+
var s = new Set();
|
|
78
|
+
for (i = 0; i < n; i++) { if (s.has(a[i])) return false; s.add(a[i]); }
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
var m = new Map();
|
|
82
|
+
for (i = 0; i < n; i++) {
|
|
83
|
+
var hh = _uqh(a[i]), b = m.get(hh);
|
|
84
|
+
if (b === undefined) m.set(hh, i);
|
|
85
|
+
else if (typeof b === 'number') { if (_deq(a[b], a[i])) return false; m.set(hh, [b, i]); }
|
|
86
|
+
else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) return false; b.push(i); }
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The duplicate pair an error reports: the first item that has a later
|
|
92
|
+
// equal, and its first later equal. Returns that later index and leaves the
|
|
93
|
+
// first in _uqpI, or returns -1 when every item is distinct.
|
|
94
|
+
var _uqpI = 0;
|
|
95
|
+
function _uqp(a) {
|
|
96
|
+
var n = a.length, i, k;
|
|
97
|
+
if (n < 2) return -1;
|
|
98
|
+
if (n <= 12) {
|
|
99
|
+
for (i = 0; i < n - 1; i++) for (k = i + 1; k < n; k++) if (_deq(a[i], a[k])) { _uqpI = i; return k; }
|
|
100
|
+
return -1;
|
|
101
|
+
}
|
|
102
|
+
var prim = true;
|
|
103
|
+
for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
|
|
104
|
+
var bi = -1, bj = -1, m = new Map();
|
|
105
|
+
if (prim) {
|
|
106
|
+
for (i = 0; i < n; i++) {
|
|
107
|
+
var p = m.get(a[i]);
|
|
108
|
+
if (p === undefined) m.set(a[i], i);
|
|
109
|
+
else if (p >= 0) { if (bi < 0 || p < bi) { bi = p; bj = i; } m.set(a[i], -1); }
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Each bucket holds the first occurrence of every distinct value that
|
|
113
|
+
// hashed there, in index order; a value is paired with its first later
|
|
114
|
+
// equal only once.
|
|
115
|
+
var paired = null;
|
|
116
|
+
for (i = 0; i < n; i++) {
|
|
117
|
+
var hh = _uqh(a[i]), b = m.get(hh), r = -1;
|
|
118
|
+
if (b === undefined) { m.set(hh, i); continue; }
|
|
119
|
+
if (typeof b === 'number') { if (_deq(a[b], a[i])) r = b; else m.set(hh, [b, i]); }
|
|
120
|
+
else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) { r = b[k]; break; } if (r < 0) b.push(i); }
|
|
121
|
+
if (r >= 0) {
|
|
122
|
+
if (paired === null) paired = new Set();
|
|
123
|
+
if (!paired.has(r)) { paired.add(r); if (bi < 0 || r < bi) { bi = r; bj = i; } }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (bi < 0) return -1;
|
|
128
|
+
_uqpI = bi;
|
|
129
|
+
return bj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// For the interpreted engine.
|
|
133
|
+
function allDistinct(a) { return _uq(a); }
|
|
134
|
+
function duplicatePair(a) {
|
|
135
|
+
var j = _uqp(a);
|
|
136
|
+
return j < 0 ? null : [_uqpI, j];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { allDistinct, duplicatePair, _deq, _uqh, _uq, _uqp };
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"release:check": "node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
|
|
65
65
|
"build": "cmake-js build --target ata",
|
|
66
66
|
"rebuild": "cmake-js rebuild --target ata",
|
|
67
|
-
"test": "node
|
|
67
|
+
"test": "node tests/run.js",
|
|
68
68
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
69
69
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
70
70
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -143,13 +143,13 @@
|
|
|
143
143
|
"LICENSE"
|
|
144
144
|
],
|
|
145
145
|
"optionalDependencies": {
|
|
146
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
147
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
148
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
149
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
150
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
151
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
152
|
-
"@ata-validator/native-win32-x64": "1.
|
|
146
|
+
"@ata-validator/native-darwin-arm64": "1.45.0",
|
|
147
|
+
"@ata-validator/native-darwin-x64": "1.45.0",
|
|
148
|
+
"@ata-validator/native-linux-arm64-gnu": "1.45.0",
|
|
149
|
+
"@ata-validator/native-linux-arm64-musl": "1.45.0",
|
|
150
|
+
"@ata-validator/native-linux-x64-gnu": "1.45.0",
|
|
151
|
+
"@ata-validator/native-linux-x64-musl": "1.45.0",
|
|
152
|
+
"@ata-validator/native-win32-x64": "1.45.0"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"yaml": "^2.0.0"
|