ata-validator 1.54.0 → 1.55.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 +28 -26
- package/error-runtime.d.ts +7 -0
- package/error-runtime.js +10 -1
- package/error-runtime.mjs +1 -1
- package/lib/aot-impl.js +26 -3
- package/lib/error-runtime.js +3 -2
- package/lib/formats-source.js +10 -3
- package/lib/formats.js +1 -1
- package/lib/interpreter-core.js +9 -2
- package/lib/plan-compiler.js +91 -28
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -138,18 +138,18 @@ errors (7.6M with `richErrors: false`).
|
|
|
138
138
|
|
|
139
139
|
| Dimension | Schema | ata-AOT | runtime validator | Difference |
|
|
140
140
|
|---|---|---|---|---|
|
|
141
|
-
| Bundle (gzipped) | simple | 1.
|
|
142
|
-
| Bundle (gzipped) | complex | 3.
|
|
143
|
-
| Bundle (gzipped) | nested | 4.
|
|
144
|
-
| Cold start | simple | 20 ms |
|
|
145
|
-
| Throughput (1M ops) | simple |
|
|
146
|
-
| Compile time | simple |
|
|
141
|
+
| Bundle (gzipped) | simple | 1.4 KB | 52.7 KB | 38.3x smaller |
|
|
142
|
+
| Bundle (gzipped) | complex | 3.1 KB | 52.7 KB | 16.8x smaller |
|
|
143
|
+
| Bundle (gzipped) | nested | 4.7 KB | 52.7 KB | 11.3x smaller |
|
|
144
|
+
| Cold start | simple | 20 ms | 38 ms | 1.9x faster |
|
|
145
|
+
| Throughput (1M ops) | simple | 308 Mops/s | 115 Mops/s | 2.7x faster |
|
|
146
|
+
| Compile time | simple | 25 µs | 1.42 ms | 56x faster |
|
|
147
147
|
|
|
148
148
|
The runtime column is the default validator most frameworks ship. Reproduce on your machine
|
|
149
|
-
with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-10-
|
|
150
|
-
on ata-validator 1.
|
|
151
|
-
97 to 110, cold start between 20 and 22 ms against 37 to 40, and the compile ratio between
|
|
152
|
-
and 65x. The compile row is the time to write a module, a build-time cost; it was 18 µs on
|
|
149
|
+
with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-10-15,
|
|
150
|
+
on ata-validator 1.54.0. On 1.51.0, across five runs, throughput moved between 263 and 325 Mops/s
|
|
151
|
+
against 97 to 110, cold start between 20 and 22 ms against 37 to 40, and the compile ratio between
|
|
152
|
+
61x and 65x. The compile row is the time to write a module, a build-time cost; it was 18 µs on
|
|
153
153
|
1.46.0, and modules have carried the one-pass error function since 1.47.0. The throughput row times a single one-million-call
|
|
154
154
|
loop of a few milliseconds, so it moves the most from run to run; treat the last three rows as
|
|
155
155
|
an order of magnitude rather than a constant.
|
|
@@ -280,21 +280,21 @@ is the most common way to get a misleading number out of this library.
|
|
|
280
280
|
|
|
281
281
|
| | compiled with `ata build` | runtime `new Validator(schema)` |
|
|
282
282
|
|---|---|---|
|
|
283
|
-
| In a bundle, gzipped | **2.
|
|
284
|
-
| Time to a served request | **3.5 ms** |
|
|
283
|
+
| In a bundle, gzipped | **2.2 KB** | 131.1 KB |
|
|
284
|
+
| Time to a served request | **3.5 ms** | 6.9 ms |
|
|
285
285
|
| Schema known when | build time | any time |
|
|
286
286
|
|
|
287
287
|
The bundle row is the ten-field user schema in
|
|
288
288
|
`tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
|
|
289
|
-
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.
|
|
289
|
+
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.54.0.
|
|
290
290
|
The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
|
|
291
|
-
`benchmark/bundle`, against 3.
|
|
291
|
+
`benchmark/bundle`, against 3.4 ms for the same app doing no validation at all, so the
|
|
292
292
|
compiled path costs nothing measurable to start. The runtime figure is what it is because
|
|
293
293
|
a schema that arrives at run time can use any keyword, so the whole engine has to be
|
|
294
294
|
there. The compiled module imports nothing and contains only the checks your schema asks
|
|
295
295
|
for.
|
|
296
296
|
|
|
297
|
-
**On a server, use whichever fits your schemas.**
|
|
297
|
+
**On a server, use whichever fits your schemas.** 131 KB of JavaScript on a Node or Bun
|
|
298
298
|
process is not a cost anyone notices, and the runtime API is the simpler thing to reach
|
|
299
299
|
for. Speed is the same either way once warm.
|
|
300
300
|
|
|
@@ -589,13 +589,15 @@ error collector out of the modules: each one carries its verdict and its schema,
|
|
|
589
589
|
its errors from one `ata-errors.mjs` the build writes beside them. The build does this only
|
|
590
590
|
where it makes the output smaller, so a single small schema keeps its self-contained module.
|
|
591
591
|
The errors are the ones `new Validator(schema, { richErrors: false })` reports, in the same
|
|
592
|
-
order. Forty SchemaStore schemas imported into one app, minified with esbuild, go from
|
|
593
|
-
to
|
|
592
|
+
order. Forty SchemaStore schemas imported into one app, minified with esbuild, go from 571.7 KB
|
|
593
|
+
to 224.9 KB gzipped, and from 61.5 ms to 29.5 ms to import with code generation blocked. A
|
|
594
594
|
rejected document costs more this way, since its errors come from an interpreted walk:
|
|
595
595
|
`--errors inline` keeps every module self-contained. `--mode schema` writes each module as
|
|
596
596
|
its schema and takes the verdict from the runtime too: forty SchemaStore schemas in one app
|
|
597
|
-
come to
|
|
598
|
-
23.
|
|
597
|
+
come to 104.4 KB gzipped against 224.9 KB as compact modules, and load and check each once in
|
|
598
|
+
23.4 ms against 35.8, while a check once warm costs about five times what generated code costs.
|
|
599
|
+
These are ata 1.54.0 figures; 1.51.0 measured 513.8, 213.3 and 102.4 KB, and the growth is the
|
|
600
|
+
RFC 3986 walk that every module with a `uri` or `uri-reference` field now carries.
|
|
599
601
|
Schemas that reference each other by `$id` are built against each other, and in schema mode
|
|
600
602
|
they are written once to a shared registry that keeps only the definitions the modules reach;
|
|
601
603
|
`--base-uri <uri>` names files that declare no `$id`. Eight Kubernetes kinds, whose schemas
|
|
@@ -603,13 +605,13 @@ share one 1.1 MB definitions file, come to 32.6 KB gzipped as schema modules aga
|
|
|
603
605
|
self-contained, on ata 1.51.0.
|
|
604
606
|
|
|
605
607
|
Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
|
|
606
|
-
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.
|
|
608
|
+
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.54.0:
|
|
607
609
|
|
|
608
610
|
| What the app imports | Size | Notes |
|
|
609
611
|
|---|---|---|
|
|
610
|
-
| `Validator` from `ata-validator` |
|
|
611
|
-
| `isValid` from the compiled module | **1.
|
|
612
|
-
| `validate` from the compiled module | **
|
|
612
|
+
| `Validator` from `ata-validator` | 131.1 KB | The compiler ships with it, because a runtime schema can use any keyword |
|
|
613
|
+
| `isValid` from the compiled module | **1.4 KB** | Nothing else is reachable, so the error collector is dropped |
|
|
614
|
+
| `validate` from the compiled module | **2.0 KB** | Adds the detailed error collector |
|
|
613
615
|
|
|
614
616
|
`--abort-early` makes the generated source about three times smaller, and after
|
|
615
617
|
bundling it makes no difference: importing only `isValid` already leaves the error
|
|
@@ -645,7 +647,7 @@ through a `setFormats()` export the module carries. `docs/API.md` has the
|
|
|
645
647
|
details.
|
|
646
648
|
|
|
647
649
|
**Fastify startup, 10 route schemas, from a cold process to the first validated request:
|
|
648
|
-
ajv
|
|
650
|
+
ajv 18.3 ms, ata 2.67 ms, no build step required.** ata registers in 0.22 ms of that and
|
|
649
651
|
compiles on the first request, so counting only registration would overstate the gap.
|
|
650
652
|
Reproduce with `node benchmark/bench_fastify_boot.mjs`.
|
|
651
653
|
|
|
@@ -754,7 +756,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
|
|
|
754
756
|
|
|
755
757
|
### Dialects
|
|
756
758
|
|
|
757
|
-
`$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1301 of 1301 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
|
|
759
|
+
`$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06, draft-04 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, draft-04 as draft-06 with `id` for `$id` and without `const`, `contains` and `propertyNames`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1301 of 1301 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
|
|
758
760
|
|
|
759
761
|
Two things differ under v1. `propertyDependencies` selects a subschema by the value of a property rather than by its presence, which is what `dependentSchemas` does. And `$dynamicRef` no longer requires bookending: the reference resolves through the dynamic scope whether or not the schema it first lands on carries a matching `$dynamicAnchor`, so the outermost matching anchor still in scope wins. Everything else ata implements is identical under both dialects, so a schema that declares no `$schema` behaves exactly as before.
|
|
760
762
|
|
|
@@ -766,7 +768,7 @@ Both are implemented in the interpreted engine, so a v1 schema that uses `$dynam
|
|
|
766
768
|
|
|
767
769
|
### Known limitations
|
|
768
770
|
|
|
769
|
-
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1301 of 1301 cases. Draft
|
|
771
|
+
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1301 of 1301 cases. Draft 2019-09 gives 1261 of 1261, draft 7 929 of 929, draft-06 841 of 841, draft-04 618 of 618. The v1 dialect gives 1135 of 1135. `npm run test:suite` reproduces all three.
|
|
770
772
|
|
|
771
773
|
Areas that remain deliberate scope decisions for 1.x:
|
|
772
774
|
|
package/error-runtime.d.ts
CHANGED
|
@@ -20,5 +20,12 @@ export function createErrors(schema: object, options?: { v1?: boolean }): (data:
|
|
|
20
20
|
/** The linear-time pattern engine compact modules share (the RegExp-like object it returns has `test`). */
|
|
21
21
|
export function compileSafe(source: string): { test(input: string): boolean };
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The RFC 3986 walk behind the `uri`, `uri-reference`, `iri` and
|
|
25
|
+
* `iri-reference` formats, which compact modules share. `mode` bit 1 asks
|
|
26
|
+
* for a scheme, bit 2 admits non-ASCII.
|
|
27
|
+
*/
|
|
28
|
+
export function uriWalk(input: string, mode: 0 | 1 | 2 | 3): boolean;
|
|
29
|
+
|
|
23
30
|
/** The ata-validator version this runtime belongs to. */
|
|
24
31
|
export const ataVersion: string;
|
package/error-runtime.js
CHANGED
|
@@ -24,6 +24,15 @@ function compileSafe(src) {
|
|
|
24
24
|
return _safe(src);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// The RFC 3986 walk behind the uri, uri-reference, iri and iri-reference
|
|
28
|
+
// formats, which a compact module imports from here instead of carrying the
|
|
29
|
+
// factory's text; `mode` as lib/uri-core.js reads it.
|
|
30
|
+
let _walk = null;
|
|
31
|
+
function uriWalk(s, mode) {
|
|
32
|
+
if (_walk === null) _walk = require('./lib/formats.js').uriWalk;
|
|
33
|
+
return _walk(s, mode);
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
// Warm-up, in a browser only: the runtime's code compiles on its first use,
|
|
28
37
|
// which a page would otherwise pay for on its first rejected document, about
|
|
29
38
|
// a millisecond. When the browser is idle, one small error list is built here
|
|
@@ -37,4 +46,4 @@ if (typeof requestIdleCallback === 'function') { try { requestIdleCallback(warm)
|
|
|
37
46
|
// once never runs it, and a server has it done before its first request.
|
|
38
47
|
else if (typeof process === 'object' && process !== null && process.versions && process.versions.node && typeof setTimeout === 'function') { try { const t = setTimeout(warm, 0); if (t && typeof t.unref === 'function') t.unref(); } catch (_) {} }
|
|
39
48
|
|
|
40
|
-
module.exports = { createErrors, compileSafe, ataVersion };
|
|
49
|
+
module.exports = { createErrors, compileSafe, uriWalk, ataVersion };
|
package/error-runtime.mjs
CHANGED
package/lib/aot-impl.js
CHANGED
|
@@ -469,12 +469,24 @@ function toStandaloneModule(validator, opts) {
|
|
|
469
469
|
for (const h of sharedDecls) errCore = errCore.split(h).join('');
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
|
+
// A compact module takes the URI walk from the runtime it imports rather
|
|
473
|
+
// than carrying the factory's text (about 1.3 KB gzipped per module).
|
|
474
|
+
let usesUriWalk = false;
|
|
475
|
+
// The texts as hoisted, which the preamble filter below matches against.
|
|
476
|
+
const sharedHoisted = sharedDecls.slice();
|
|
477
|
+
if (errorRuntime !== null && sharedDecls.length) {
|
|
478
|
+
const fs0 = require('./formats-source');
|
|
479
|
+
const full = fs0.uriHelperSource('_uri');
|
|
480
|
+
for (let i = 0; i < sharedDecls.length; i++) {
|
|
481
|
+
if (sharedDecls[i] === full) { sharedDecls[i] = fs0.uriHelperCompactSource('_uri'); usesUriWalk = true; }
|
|
482
|
+
}
|
|
483
|
+
}
|
|
472
484
|
const sharedBlock = sharedDecls.length ? sharedDecls.join('\n') + '\n' : '';
|
|
473
485
|
|
|
474
486
|
// Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
|
|
475
487
|
// runtime emits them before the function). The standalone module must declare
|
|
476
488
|
// them at module scope too, or _fn references undefined names (e.g. _af1_b0).
|
|
477
|
-
const sharedSet = new Set(
|
|
489
|
+
const sharedSet = new Set(sharedHoisted);
|
|
478
490
|
const preambleParts = jsFn._preambleParts
|
|
479
491
|
? jsFn._preambleParts.filter((part) => !sharedSet.has(part))
|
|
480
492
|
: null;
|
|
@@ -631,7 +643,7 @@ function validateJSON(text) {
|
|
|
631
643
|
${errorRuntime === null ? '// Schema is embedded; runtime has zero dependency on ata-validator.' : '// Compact module: the verdict and the schema; errors come from the runtime it imports.'}
|
|
632
644
|
// Re-run the build after upgrading ata: the version above is what wrote this.
|
|
633
645
|
${degradedNote}'use strict';
|
|
634
|
-
${errorRuntime === null ? '' : format === 'esm' ? `import { createErrors as _ataCreateErrors${usesSafeRe ? ', compileSafe as _ataCompileSafe' : ''} } from ${JSON.stringify(errorRuntime)};\n` : `const { createErrors: _ataCreateErrors${usesSafeRe ? ', compileSafe: _ataCompileSafe' : ''} } = require(${JSON.stringify(errorRuntime)});\n`}${_CP_LEN_SOURCE}
|
|
646
|
+
${errorRuntime === null ? '' : format === 'esm' ? `import { createErrors as _ataCreateErrors${usesSafeRe ? ', compileSafe as _ataCompileSafe' : ''}${usesUriWalk ? ', uriWalk as _ataUriWalk' : ''} } from ${JSON.stringify(errorRuntime)};\n` : `const { createErrors: _ataCreateErrors${usesSafeRe ? ', compileSafe: _ataCompileSafe' : ''}${usesUriWalk ? ', uriWalk: _ataUriWalk' : ''} } = require(${JSON.stringify(errorRuntime)});\n`}${_CP_LEN_SOURCE}
|
|
635
647
|
${errorRuntime !== null ? (usesSafeRe ? 'const __ataSafeRe = _ataCompileSafe;\n' : '') : safeRePrelude(jsFn, jsErrFn, onePass !== null && onePass.usesSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
636
648
|
const ABORT = Object.freeze({
|
|
637
649
|
valid: false,
|
|
@@ -938,7 +950,12 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
938
950
|
return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
939
951
|
});
|
|
940
952
|
const arr = `[${fns.join(',')}]`;
|
|
941
|
-
|
|
953
|
+
// The code-point counter the length checks call. toStandaloneModule embeds
|
|
954
|
+
// it; the bundles did not, so a bundled minLength or maxLength threw a
|
|
955
|
+
// ReferenceError on the first string whose length its fast bounds left
|
|
956
|
+
// open (one character under minLength: 1, for one).
|
|
957
|
+
const cpLenEmbed = /\b_cpLen\(/.test(arr) && !/function _cpLen\b/.test(arr) ? _CP_LEN_SOURCE + '\n' : '';
|
|
958
|
+
const safeEmbed = (bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '') + cpLenEmbed +
|
|
942
959
|
(sharedDecls.length ? sharedDecls.join('\n') + '\n' : '');
|
|
943
960
|
const registry = bundleInjects
|
|
944
961
|
? `var __formats=Object.create(null);\nfunction setFormats(map){for(var k in map)__formats[k]=map[k]}\n`
|
|
@@ -1108,6 +1125,12 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
1108
1125
|
if (fmtOut.exportsSetFormats) out += 'module.exports.setFormats=setFormats;\n';
|
|
1109
1126
|
}
|
|
1110
1127
|
|
|
1128
|
+
// The code-point counter, as in bundleStandalone.
|
|
1129
|
+
if (/\b_cpLen\(/.test(out) && !/function _cpLen\b/.test(out)) {
|
|
1130
|
+
const head = isEsm ? 0 : out.indexOf("'use strict';\n") + "'use strict';\n".length;
|
|
1131
|
+
const at = isEsm ? out.indexOf('\n') + 1 : head;
|
|
1132
|
+
out = out.slice(0, at) + _CP_LEN_SOURCE + '\n' + out.slice(at);
|
|
1133
|
+
}
|
|
1111
1134
|
return out;
|
|
1112
1135
|
}
|
|
1113
1136
|
|
package/lib/error-runtime.js
CHANGED
|
@@ -134,6 +134,7 @@ function _ataCode(k, f) {
|
|
|
134
134
|
// The modules are evaluated on the first call, not when this file loads.
|
|
135
135
|
function createErrors(schema, options) { return _ataRequire('error-runtime-entry').createErrors(schema, _ataCode, options); }
|
|
136
136
|
function compileSafe(src) { return _ataRequire('safe-regex').compileSafe(src); }
|
|
137
|
+
function uriWalk(s, m) { return _ataRequire('formats').uriWalk(s, m); }
|
|
137
138
|
${schemaMode ? "function createValidator(schema, options) { return _ataRequire('schema-runtime-entry').createValidator(schema, _ataCode, options); }\n" : ''}
|
|
138
139
|
const ataVersion = ${JSON.stringify(version)};
|
|
139
140
|
|
|
@@ -151,8 +152,8 @@ if (typeof requestIdleCallback === 'function') { try { requestIdleCallback(warm)
|
|
|
151
152
|
else if (typeof process === 'object' && process !== null && process.versions && process.versions.node && typeof setTimeout === 'function') { try { const t = setTimeout(warm, 0); if (t && typeof t.unref === 'function') t.unref(); } catch (_) {} }
|
|
152
153
|
`;
|
|
153
154
|
const out = format === 'cjs'
|
|
154
|
-
? body + `module.exports = { createErrors, compileSafe, ${schemaMode ? 'createValidator, ' : ''}ataVersion };\n`
|
|
155
|
-
: body + `export { createErrors, compileSafe, ${schemaMode ? 'createValidator, ' : ''}ataVersion };\n`;
|
|
155
|
+
? body + `module.exports = { createErrors, compileSafe, uriWalk, ${schemaMode ? 'createValidator, ' : ''}ataVersion };\n`
|
|
156
|
+
: body + `export { createErrors, compileSafe, uriWalk, ${schemaMode ? 'createValidator, ' : ''}ataVersion };\n`;
|
|
156
157
|
_cache.set(key, out);
|
|
157
158
|
return out;
|
|
158
159
|
}
|
package/lib/formats-source.js
CHANGED
|
@@ -166,10 +166,17 @@ const uriWalkText = () => _uriSrc !== null ? _uriSrc : (_uriSrc = require('./uri
|
|
|
166
166
|
const uriWalkSource = () => `(${uriWalkText()})(function(_s){${ipv6Source('_s', true)}return true})`
|
|
167
167
|
// `_uri(s)` is the uri check, `_urif(s, mode)` the other three (the
|
|
168
168
|
// expression in front for an iri), `_uriw(s, mode)` the walk alone.
|
|
169
|
-
const
|
|
170
|
-
'const _ufa=/' + _formats().URI_FAST.source + '/;const ' + name + 'w=' + uriWalkSource() + ';' +
|
|
169
|
+
const uriHelperTail = (name) =>
|
|
171
170
|
'function ' + name + '(_s){if(_ufa.test(_s))return true;return ' + name + 'w(_s,1)}' +
|
|
172
171
|
'function ' + name + 'f(_s,_m){if((_m&1)===1&&_ufa.test(_s))return true;return ' + name + 'w(_s,_m)}'
|
|
172
|
+
const uriHelperSource = (name) =>
|
|
173
|
+
'const _ufa=/' + _formats().URI_FAST.source + '/;const ' + name + 'w=' + uriWalkSource() + ';' + uriHelperTail(name)
|
|
174
|
+
// The same helper for a compact module, which imports the walk from the
|
|
175
|
+
// error runtime it already loads (`uriWalk as _ataUriWalk`) instead of
|
|
176
|
+
// carrying the factory's text: forty SchemaStore schemas built that way
|
|
177
|
+
// carried forty copies.
|
|
178
|
+
const uriHelperCompactSource = (name) =>
|
|
179
|
+
'const _ufa=/' + _formats().URI_FAST.source + '/;const ' + name + 'w=_ataUriWalk;' + uriHelperTail(name)
|
|
173
180
|
// The inline forms, for a caller with no function to hoist into: the walk is
|
|
174
181
|
// built in place, which costs a factory per call, so the generators hoist.
|
|
175
182
|
function uriSource (v, isStr) { return guard(v, isStr, `if(!/${_formats().URI_FAST.source}/.test(${v})&&!${uriWalkSource()}(${v},1))return false`) }
|
|
@@ -297,4 +304,4 @@ function durationSource (v, isStr) {
|
|
|
297
304
|
const inner = `if(!${_formats().DURATION_RE.toString()}.test(${v}))return false`;
|
|
298
305
|
return isStr ? inner : `if(typeof ${v}==='string'&&${inner.slice(3)}`;
|
|
299
306
|
}
|
|
300
|
-
module.exports = { uriHelperSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uriSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
|
|
307
|
+
module.exports = { uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uriSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
|
package/lib/formats.js
CHANGED
|
@@ -538,4 +538,4 @@ function email(s) {
|
|
|
538
538
|
const DURATION_RE = /^P(?:\d+W|(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)(?:T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))?|T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))$/;
|
|
539
539
|
function duration (s) { return DURATION_RE.test(s); }
|
|
540
540
|
|
|
541
|
-
module.exports = { IPV4, IPV6_FULL, ipv4Range, URI_FAST, date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uuid, time, noReserved, jsonPointer, relativeJsonPointer, uriTemplate, email, duration, iri, iriReference, idnEmail, DURATION_RE, URI_TEMPLATE_EXPR };
|
|
541
|
+
module.exports = { IPV4, IPV6_FULL, ipv4Range, URI_FAST, date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uriWalk: _uriWalk, uuid, time, noReserved, jsonPointer, relativeJsonPointer, uriTemplate, email, duration, iri, iriReference, idnEmail, DURATION_RE, URI_TEMPLATE_EXPR };
|
package/lib/interpreter-core.js
CHANGED
|
@@ -876,7 +876,14 @@ class Interpreter {
|
|
|
876
876
|
if (this._fast === undefined) {
|
|
877
877
|
const compile = planCompiler();
|
|
878
878
|
if (compile === null) this._fast = null;
|
|
879
|
-
else
|
|
879
|
+
else {
|
|
880
|
+
try { this._fast = compile(this); } catch (e) {
|
|
881
|
+
// The plain walk answers instead, which tests cannot tell apart;
|
|
882
|
+
// tests/test_plan_compiles.js holds the compiler to the suite.
|
|
883
|
+
if (typeof process === 'object' && process !== null && process.env && process.env.ATA_DEBUG) console.error('ata: plan compiler failed, plain walk used:', e && e.message);
|
|
884
|
+
this._fast = null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
880
887
|
}
|
|
881
888
|
return this._fast;
|
|
882
889
|
}
|
|
@@ -1502,7 +1509,7 @@ function planCompiler() {
|
|
|
1502
1509
|
compileInterpreter = loadPlanCompiler()({
|
|
1503
1510
|
Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
1504
1511
|
deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
|
|
1505
|
-
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
|
|
1512
|
+
T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri,
|
|
1506
1513
|
}).compileInterpreter;
|
|
1507
1514
|
}
|
|
1508
1515
|
return compileInterpreter;
|
package/lib/plan-compiler.js
CHANGED
|
@@ -53,7 +53,7 @@ const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string'
|
|
|
53
53
|
function install(deps) {
|
|
54
54
|
const { Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
55
55
|
deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
|
|
56
|
-
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
|
|
56
|
+
T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, 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
|
|
@@ -169,12 +169,21 @@ function install(deps) {
|
|
|
169
169
|
}
|
|
170
170
|
if (P.enum !== null) {
|
|
171
171
|
if (P.enum.length === 0) checks.push(() => false);
|
|
172
|
+
else if (P.enumObjs.length === 0) { const prims = P.enumPrims; checks.push((data) => prims.has(data)); }
|
|
172
173
|
else checks.push((data) => enumMember(P, data));
|
|
173
174
|
}
|
|
174
175
|
if (P.hasConst) {
|
|
175
176
|
const c = P.const;
|
|
176
177
|
checks.push((data) => deepEqual(c, data));
|
|
177
178
|
}
|
|
179
|
+
// The value checks of each type, folded to one call or null. A node of
|
|
180
|
+
// one type and nothing but that type's keywords (the common leaf: a
|
|
181
|
+
// string with a length, an integer with a bound) is answered by one
|
|
182
|
+
// closure over typeof and that call, with no type bits computed and no
|
|
183
|
+
// chain of guarded checks: the interpreted engine spent a fifth of a
|
|
184
|
+
// document in those calls.
|
|
185
|
+
const reqFolded = P.required !== null && P.requiredUndef === null && P.properties !== null;
|
|
186
|
+
let numsRun = null;
|
|
178
187
|
if (P.hasNumber) {
|
|
179
188
|
const nums = [];
|
|
180
189
|
if (P.minimum !== undefined) { const m = P.minimum; nums.push((d) => d >= m); }
|
|
@@ -182,36 +191,32 @@ function install(deps) {
|
|
|
182
191
|
if (P.exclusiveMinimum !== undefined) { const m = P.exclusiveMinimum; nums.push((d) => d > m); }
|
|
183
192
|
if (P.exclusiveMaximum !== undefined) { const m = P.exclusiveMaximum; nums.push((d) => d < m); }
|
|
184
193
|
if (P.multipleOf !== undefined) { const m = P.multipleOf; nums.push((d) => multipleOfOk(d, m)); }
|
|
185
|
-
|
|
186
|
-
// A non-finite number is not of type `number` here: dataBits() gives it
|
|
187
|
-
// no type bit, and an empty schema accepts it. So the numeric keywords
|
|
188
|
-
// must not apply to it either, or `{ minimum: 3 }` rejects a value that
|
|
189
|
-
// `{}` accepts and the compiled engine admits.
|
|
190
|
-
if (run !== null) checks.push((data) => !Number.isFinite(data) || run(data));
|
|
194
|
+
numsRun = combineValue(nums);
|
|
191
195
|
}
|
|
196
|
+
let strsRun = null;
|
|
192
197
|
if (P.hasString) {
|
|
193
198
|
const strs = [];
|
|
194
|
-
if (P.minLength !== undefined
|
|
195
|
-
|
|
199
|
+
if (P.minLength !== undefined && P.maxLength !== undefined) {
|
|
200
|
+
const lo = P.minLength, hi = P.maxLength;
|
|
201
|
+
strs.push((d) => { const n = d.length; return (n >= 2 * lo && n <= hi) || (cpAtLeast(d, lo) && cpAtMost(d, hi)); });
|
|
202
|
+
} else if (P.minLength !== undefined) { const m = P.minLength; strs.push((d) => cpAtLeast(d, m)); }
|
|
203
|
+
else if (P.maxLength !== undefined) { const m = P.maxLength; strs.push((d) => cpAtMost(d, m)); }
|
|
196
204
|
if (P.pattern !== null) { const re = P.pattern; strs.push((d) => re.test(d)); }
|
|
197
205
|
if (P.formatFn !== null) { const f = P.formatFn; strs.push((d) => f(d)); }
|
|
198
|
-
|
|
199
|
-
if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_STRING || run(data)); }
|
|
206
|
+
strsRun = combineValue(strs);
|
|
200
207
|
}
|
|
201
|
-
|
|
202
|
-
// keywords; only the three value-level ones belong here, exactly as
|
|
203
|
-
// evalLeafV() reads them.
|
|
208
|
+
let arrsRun = null;
|
|
204
209
|
if (P.minItems !== undefined || P.maxItems !== undefined || P.uniqueItems) {
|
|
205
210
|
const arrs = [];
|
|
206
211
|
if (P.minItems !== undefined) { const m = P.minItems; arrs.push((d) => d.length >= m); }
|
|
207
212
|
if (P.maxItems !== undefined) { const m = P.maxItems; arrs.push((d) => d.length <= m); }
|
|
208
213
|
if (P.uniqueItems) arrs.push(allDistinct);
|
|
209
|
-
|
|
210
|
-
if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_ARRAY || run(data)); }
|
|
214
|
+
arrsRun = combineValue(arrs);
|
|
211
215
|
}
|
|
212
|
-
|
|
216
|
+
let objsRun = null;
|
|
217
|
+
if ((P.required !== null && !reqFolded) || P.minProperties !== undefined || P.maxProperties !== undefined || P.dependentRequired !== null) {
|
|
213
218
|
const objs = [];
|
|
214
|
-
if (P.required !== null) {
|
|
219
|
+
if (P.required !== null && !reqFolded) {
|
|
215
220
|
const req = P.required;
|
|
216
221
|
// A required key holding undefined is missing; for a declared
|
|
217
222
|
// property the property step finds it (requiredInProps).
|
|
@@ -239,9 +244,34 @@ function install(deps) {
|
|
|
239
244
|
return true;
|
|
240
245
|
});
|
|
241
246
|
}
|
|
242
|
-
|
|
243
|
-
if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_OBJECT || run(data)); }
|
|
247
|
+
objsRun = combineValue(objs);
|
|
244
248
|
}
|
|
249
|
+
if (P.hasType && P.enum === null && !P.hasConst && !P.hasCustom && checks.length === 1) {
|
|
250
|
+
const m = P.typeMask;
|
|
251
|
+
// The run of the type's own keywords, and whether any other type's are present.
|
|
252
|
+
const own = m === T_STRING ? strsRun : m === T_INTEGER || m === T_NUMBER ? numsRun : m === T_OBJECT ? objsRun : m === T_ARRAY ? arrsRun : null;
|
|
253
|
+
const foreign = (numsRun !== null && m !== T_INTEGER && m !== T_NUMBER) || (strsRun !== null && m !== T_STRING) || (arrsRun !== null && m !== T_ARRAY) || (objsRun !== null && m !== T_OBJECT);
|
|
254
|
+
if (!foreign) {
|
|
255
|
+
const r = own;
|
|
256
|
+
switch (m) {
|
|
257
|
+
case T_STRING: return r === null ? (d) => typeof d === 'string' : (d) => typeof d === 'string' && r(d);
|
|
258
|
+
case T_INTEGER: return r === null ? (d) => Number.isInteger(d) : (d) => Number.isInteger(d) && r(d);
|
|
259
|
+
case T_NUMBER: return r === null ? (d) => typeof d === 'number' && isFinite(d) : (d) => typeof d === 'number' && isFinite(d) && r(d);
|
|
260
|
+
case T_BOOLEAN: return (d) => typeof d === 'boolean';
|
|
261
|
+
case T_NULL: return (d) => d === null;
|
|
262
|
+
case T_OBJECT: return r === null ? (d) => typeof d === 'object' && d !== null && !Array.isArray(d) : (d) => typeof d === 'object' && d !== null && !Array.isArray(d) && r(d);
|
|
263
|
+
case T_ARRAY: return r === null ? (d) => Array.isArray(d) : (d) => Array.isArray(d) && r(d);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// A non-finite number is not of type `number` here: dataBits() gives it
|
|
268
|
+
// no type bit, and an empty schema accepts it. So the numeric keywords
|
|
269
|
+
// must not apply to it either, or `{ minimum: 3 }` rejects a value that
|
|
270
|
+
// `{}` accepts and the compiled engine admits.
|
|
271
|
+
if (numsRun !== null) { const run = numsRun; checks.push((data) => !Number.isFinite(data) || run(data)); }
|
|
272
|
+
if (strsRun !== null) { const run = strsRun; needsBits = true; checks.push((data, bits) => bits !== T_STRING || run(data)); }
|
|
273
|
+
if (arrsRun !== null) { const run = arrsRun; needsBits = true; checks.push((data, bits) => bits !== T_ARRAY || run(data)); }
|
|
274
|
+
if (objsRun !== null) { const run = objsRun; needsBits = true; checks.push((data, bits) => bits !== T_OBJECT || run(data)); }
|
|
245
275
|
|
|
246
276
|
if (P.hasCustom) {
|
|
247
277
|
needsBits = true;
|
|
@@ -317,6 +347,7 @@ function install(deps) {
|
|
|
317
347
|
|
|
318
348
|
const steps = []; // collect variant: (data, errors, ip, sp, stack, rec)
|
|
319
349
|
const vsteps = []; // verdict variant: (data, stack, rec)
|
|
350
|
+
let leafV = TRUE_FN;
|
|
320
351
|
|
|
321
352
|
// $ref / $dynamicRef, resolved at compile time.
|
|
322
353
|
for (let which = 0; which < 2; which++) {
|
|
@@ -392,7 +423,8 @@ function install(deps) {
|
|
|
392
423
|
P.required !== null || P.minProperties !== undefined || P.maxProperties !== undefined ||
|
|
393
424
|
P.dependentRequired !== null || P.hasCustom) {
|
|
394
425
|
steps.push((data, errors, instancePath, schemaPath) => evalLeaf(P, data, errors, instancePath, schemaPath));
|
|
395
|
-
|
|
426
|
+
leafV = compileLeafV(P);
|
|
427
|
+
if (leafV !== TRUE_FN) vsteps.push(leafV);
|
|
396
428
|
}
|
|
397
429
|
|
|
398
430
|
// Arrays
|
|
@@ -512,6 +544,10 @@ function install(deps) {
|
|
|
512
544
|
? P.patternProperties.map((e) => ({ re: e.re, src: e.src, fn: child(e.node) }))
|
|
513
545
|
: null;
|
|
514
546
|
const apFn = P.additionalProperties !== undefined ? child(P.additionalProperties) : null;
|
|
547
|
+
// The node's value checks, when they are the step before this one,
|
|
548
|
+
// run inside it: one closure call per object instead of two, and the
|
|
549
|
+
// composition above it goes away.
|
|
550
|
+
const fusedLeaf = vsteps.length > 0 && vsteps[vsteps.length - 1] === leafV && leafV !== TRUE_FN ? vsteps.pop() : null;
|
|
515
551
|
// `additionalProperties: false` is a boolean subschema, but its error is
|
|
516
552
|
// not the generic "boolean schema is false": every consumer switches on
|
|
517
553
|
// keyword === 'additionalProperties' and reads params.additionalProperty
|
|
@@ -591,24 +627,49 @@ function install(deps) {
|
|
|
591
627
|
// not mistaken for data. A long list walks the data's keys instead,
|
|
592
628
|
// since the data usually carries far fewer.
|
|
593
629
|
const n = propKeys.length;
|
|
630
|
+
// On an ordinary object (its prototype is Object.prototype) a key
|
|
631
|
+
// that names nothing on that prototype is read directly: a value
|
|
632
|
+
// means the key is the object's own, undefined means absent or held
|
|
633
|
+
// undefined, which the schema treats alike. Other objects, and keys
|
|
634
|
+
// such as `constructor` or `toString`, keep the own-property test.
|
|
635
|
+
const direct = propKeys.map((k) => !(k in Object.prototype));
|
|
636
|
+
const assertsObject = fusedLeaf !== null && P.hasType && P.typeMask === T_OBJECT;
|
|
594
637
|
vsteps.push((data, stack, rec) => {
|
|
595
|
-
if (
|
|
638
|
+
if (fusedLeaf !== null && !fusedLeaf(data)) return false;
|
|
639
|
+
if (!assertsObject && dataBits(data) !== T_OBJECT) return true;
|
|
640
|
+
const plain = Object.getPrototypeOf(data) === Object.prototype;
|
|
596
641
|
for (let i = 0; i < n; i++) {
|
|
597
642
|
const key = propKeys[i];
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
643
|
+
let value;
|
|
644
|
+
if (plain && direct[i]) {
|
|
645
|
+
value = data[key];
|
|
646
|
+
// Absent or held undefined: missing when required (the leaf
|
|
647
|
+
// step no longer checks it, reqFolded in compileLeafV). A key
|
|
648
|
+
// held undefined still counts as evaluated for the record.
|
|
649
|
+
if (value === undefined) { if (propList[i].req) return false; if (collect && Object.hasOwn(data, key)) addProp(rec, key); continue; }
|
|
650
|
+
} else {
|
|
651
|
+
if (!Object.hasOwn(data, key)) { if (propList[i].req) return false; continue; }
|
|
652
|
+
value = data[key];
|
|
653
|
+
if (value === undefined) { if (propList[i].req) return false; if (collect) addProp(rec, key); continue; }
|
|
654
|
+
}
|
|
655
|
+
if (!propList[i].fn.v(value, stack)) return false;
|
|
602
656
|
if (collect) addProp(rec, key);
|
|
603
657
|
}
|
|
604
658
|
return true;
|
|
605
659
|
});
|
|
606
|
-
} else
|
|
660
|
+
} else {
|
|
661
|
+
// Required keys that are declared properties are counted as the walk
|
|
662
|
+
// meets them (reqFolded in compileLeafV); one short at the end means
|
|
663
|
+
// one is missing.
|
|
664
|
+
const reqN = P.required !== null && P.requiredUndef === null && P.requiredInProps !== null ? P.requiredInProps.size : 0;
|
|
665
|
+
vsteps.push((data, stack, rec) => {
|
|
666
|
+
if (fusedLeaf !== null && !fusedLeaf(data)) return false;
|
|
607
667
|
if (dataBits(data) !== T_OBJECT) return true;
|
|
608
668
|
const keys = collect ? keysOf(rec, data) : Object.keys(data);
|
|
609
669
|
if (pnFn !== null) {
|
|
610
670
|
for (const k of keys) if (!pnFn.v(k, stack)) return false;
|
|
611
671
|
}
|
|
672
|
+
let seenReq = 0;
|
|
612
673
|
for (let k = 0; k < keys.length; k++) {
|
|
613
674
|
const key = keys[k];
|
|
614
675
|
let evaluated = false;
|
|
@@ -617,6 +678,7 @@ function install(deps) {
|
|
|
617
678
|
const value = data[key];
|
|
618
679
|
if (value !== undefined) { if (!prop.fn.v(value, stack)) return false; }
|
|
619
680
|
else if (prop.req) return false;
|
|
681
|
+
if (prop.req) seenReq++;
|
|
620
682
|
evaluated = true;
|
|
621
683
|
}
|
|
622
684
|
if (patterns !== null) {
|
|
@@ -634,8 +696,9 @@ function install(deps) {
|
|
|
634
696
|
}
|
|
635
697
|
if (evaluated && collect) addProp(rec, key);
|
|
636
698
|
}
|
|
637
|
-
return
|
|
699
|
+
return seenReq === reqN;
|
|
638
700
|
});
|
|
701
|
+
}
|
|
639
702
|
}
|
|
640
703
|
if (P.dependentSchemas !== null) {
|
|
641
704
|
const entries = P.dependentSchemas.map(([k, v]) => [k, inPlace(v, base), escapePointer(k)]);
|
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.55.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
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -174,13 +174,13 @@
|
|
|
174
174
|
"LICENSE"
|
|
175
175
|
],
|
|
176
176
|
"optionalDependencies": {
|
|
177
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
178
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
179
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
180
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
181
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
182
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
183
|
-
"@ata-validator/native-win32-x64": "1.
|
|
177
|
+
"@ata-validator/native-darwin-arm64": "1.55.0",
|
|
178
|
+
"@ata-validator/native-darwin-x64": "1.55.0",
|
|
179
|
+
"@ata-validator/native-linux-arm64-gnu": "1.55.0",
|
|
180
|
+
"@ata-validator/native-linux-arm64-musl": "1.55.0",
|
|
181
|
+
"@ata-validator/native-linux-x64-gnu": "1.55.0",
|
|
182
|
+
"@ata-validator/native-linux-x64-musl": "1.55.0",
|
|
183
|
+
"@ata-validator/native-win32-x64": "1.55.0"
|
|
184
184
|
},
|
|
185
185
|
"peerDependencies": {
|
|
186
186
|
"yaml": "^2.0.0"
|