ata-validator 1.53.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 +35 -26
- package/compat.js +13 -2
- 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/codegen-engine.js +2 -1
- package/lib/dialect.js +36 -1
- package/lib/draft7.js +17 -28
- package/lib/enrich-site.js +29 -2
- package/lib/error-runtime.js +9 -4
- package/lib/formats-source.js +50 -66
- package/lib/formats.js +37 -231
- package/lib/interpreter-compat.js +144 -0
- package/lib/interpreter-core.js +55 -144
- package/lib/js-compiler-errors.js +52 -16
- package/lib/js-compiler.js +537 -65
- package/lib/metaschemas.js +5 -3
- package/lib/plan-compiler.js +113 -33
- package/lib/regex-linear.js +81 -1
- package/lib/safe-regex.js +14 -11
- package/lib/schema-scan.js +19 -28
- package/lib/tier0.js +1 -0
- package/lib/unique-items.js +16 -5
- package/lib/uri-core.js +155 -0
- package/lib/validator-core.js +20 -11
- package/lib/version.js +1 -1
- package/package.json +9 -9
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,24 +280,31 @@ 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
|
|
|
301
|
+
**On Node 22 or later, a process that starts often can keep V8's compiled code between
|
|
302
|
+
starts.** Point `NODE_COMPILE_CACHE` at a directory that survives the restart, or call
|
|
303
|
+
`module.enableCompileCache()` in your entry. Measured on an Apple M4 Pro, Node 25.2.1:
|
|
304
|
+
loading ata goes from 4.9 to 2.7 ms, and a fresh process reaches its first validated
|
|
305
|
+
document in 5.3 ms instead of 7.8. This is Node's feature, it covers every module the
|
|
306
|
+
process loads, and ata does not turn it on for you.
|
|
307
|
+
|
|
301
308
|
**In a browser, on an edge runtime, or anywhere cold starts are charged, compile.** This
|
|
302
309
|
is where the difference is the whole story, and it is also where `new Function` is often
|
|
303
310
|
blocked outright, which the compiled module does not need. With a bundler, adding `@ata-project/unplugin`
|
|
@@ -582,13 +589,15 @@ error collector out of the modules: each one carries its verdict and its schema,
|
|
|
582
589
|
its errors from one `ata-errors.mjs` the build writes beside them. The build does this only
|
|
583
590
|
where it makes the output smaller, so a single small schema keeps its self-contained module.
|
|
584
591
|
The errors are the ones `new Validator(schema, { richErrors: false })` reports, in the same
|
|
585
|
-
order. Forty SchemaStore schemas imported into one app, minified with esbuild, go from
|
|
586
|
-
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
|
|
587
594
|
rejected document costs more this way, since its errors come from an interpreted walk:
|
|
588
595
|
`--errors inline` keeps every module self-contained. `--mode schema` writes each module as
|
|
589
596
|
its schema and takes the verdict from the runtime too: forty SchemaStore schemas in one app
|
|
590
|
-
come to
|
|
591
|
-
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.
|
|
592
601
|
Schemas that reference each other by `$id` are built against each other, and in schema mode
|
|
593
602
|
they are written once to a shared registry that keeps only the definitions the modules reach;
|
|
594
603
|
`--base-uri <uri>` names files that declare no `$id`. Eight Kubernetes kinds, whose schemas
|
|
@@ -596,13 +605,13 @@ share one 1.1 MB definitions file, come to 32.6 KB gzipped as schema modules aga
|
|
|
596
605
|
self-contained, on ata 1.51.0.
|
|
597
606
|
|
|
598
607
|
Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
|
|
599
|
-
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:
|
|
600
609
|
|
|
601
610
|
| What the app imports | Size | Notes |
|
|
602
611
|
|---|---|---|
|
|
603
|
-
| `Validator` from `ata-validator` |
|
|
604
|
-
| `isValid` from the compiled module | **1.
|
|
605
|
-
| `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 |
|
|
606
615
|
|
|
607
616
|
`--abort-early` makes the generated source about three times smaller, and after
|
|
608
617
|
bundling it makes no difference: importing only `isValid` already leaves the error
|
|
@@ -638,7 +647,7 @@ through a `setFormats()` export the module carries. `docs/API.md` has the
|
|
|
638
647
|
details.
|
|
639
648
|
|
|
640
649
|
**Fastify startup, 10 route schemas, from a cold process to the first validated request:
|
|
641
|
-
ajv
|
|
650
|
+
ajv 18.3 ms, ata 2.67 ms, no build step required.** ata registers in 0.22 ms of that and
|
|
642
651
|
compiles on the first request, so counting only registration would overstate the gap.
|
|
643
652
|
Reproduce with `node benchmark/bench_fastify_boot.mjs`.
|
|
644
653
|
|
|
@@ -747,7 +756,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
|
|
|
747
756
|
|
|
748
757
|
### Dialects
|
|
749
758
|
|
|
750
|
-
`$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.
|
|
751
760
|
|
|
752
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.
|
|
753
762
|
|
|
@@ -759,7 +768,7 @@ Both are implemented in the interpreted engine, so a v1 schema that uses `$dynam
|
|
|
759
768
|
|
|
760
769
|
### Known limitations
|
|
761
770
|
|
|
762
|
-
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.
|
|
763
772
|
|
|
764
773
|
Areas that remain deliberate scope decisions for 1.x:
|
|
765
774
|
|
package/compat.js
CHANGED
|
@@ -21,6 +21,9 @@ const { resolvePointer } = require('./lib/pointer.js');
|
|
|
21
21
|
// `strictTuples` and `strictRequired` are accepted and ignored.
|
|
22
22
|
|
|
23
23
|
const { Validator } = require('./index');
|
|
24
|
+
// The interpreted engine's coercion where each type is checked and its
|
|
25
|
+
// discriminator, which only compat asks for (see lib/interpreter-compat.js).
|
|
26
|
+
require('./lib/interpreter-core')._setCompat(require('./lib/interpreter-compat'));
|
|
24
27
|
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
25
28
|
const { normalizeExclusiveBounds } = require('./lib/draft7');
|
|
26
29
|
const { createShaper, asReferenceKeyword } = require('./lib/compat-errors');
|
|
@@ -680,8 +683,16 @@ function fragmentKey(key) {
|
|
|
680
683
|
// What a schema holds anywhere below it, read from the walk normalization
|
|
681
684
|
// makes of it anyway (lib/schema-scan.js), which remembers it per object.
|
|
682
685
|
const _scan = require('./lib/schema-scan');
|
|
683
|
-
const F_BEYOND =
|
|
684
|
-
|
|
686
|
+
const F_REF = 1 << 16, F_BEYOND = 1 << 17, F_DISCR = 1 << 18, F_DEFAULT = 1 << 19, F_ALLOF = 1 << 20, F_FORMAT = 1 << 21;
|
|
687
|
+
_scan._setExtraKeys(new Map([
|
|
688
|
+
['$ref', F_REF | F_BEYOND], ['$dynamicRef', F_BEYOND], ['allOf', F_ALLOF | F_BEYOND],
|
|
689
|
+
['anyOf', F_BEYOND], ['oneOf', F_BEYOND], ['not', F_BEYOND], ['if', F_BEYOND],
|
|
690
|
+
['then', F_BEYOND], ['else', F_BEYOND], ['additionalProperties', F_BEYOND],
|
|
691
|
+
['patternProperties', F_BEYOND], ['prefixItems', F_BEYOND], ['additionalItems', F_BEYOND],
|
|
692
|
+
['contains', F_BEYOND], ['dependentSchemas', F_BEYOND], ['dependencies', F_BEYOND],
|
|
693
|
+
['propertyNames', F_BEYOND], ['unevaluatedProperties', F_BEYOND], ['unevaluatedItems', F_BEYOND],
|
|
694
|
+
['discriminator', F_DISCR], ['default', F_DEFAULT], ['format', F_FORMAT],
|
|
695
|
+
]));
|
|
685
696
|
const features = _scan.scan;
|
|
686
697
|
|
|
687
698
|
const _metaValidators = new Map();
|
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/codegen-engine.js
CHANGED
|
@@ -69,7 +69,8 @@ function richBuilder(combRich, root, presentFallback, fallback, plain, cleanPlai
|
|
|
69
69
|
return (data) => {
|
|
70
70
|
const r = combRich(data);
|
|
71
71
|
if (r.valid || !r.errors || !r.errors.length) return presentFallback(fallback, data);
|
|
72
|
-
|
|
72
|
+
// `mo`: the list came out in the reader's order (see compileToJSCombined).
|
|
73
|
+
const out = r.mo === true || !_needsOrdering(r.errors) ? r.errors : _sortErrors(root, r.errors);
|
|
73
74
|
// A collapsed oneOf or anyOf comes out plain (no docUrl) and is enriched
|
|
74
75
|
// here, its branch errors with it, as presentErrors would.
|
|
75
76
|
for (let i = 0; i < out.length; i++) {
|
package/lib/dialect.js
CHANGED
|
@@ -28,4 +28,39 @@ function isV1Dialect(schema) {
|
|
|
28
28
|
return V1_DIALECTS.has(uri);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// The earlier dialects by their meta-schema URIs. draftCode is asked of every
|
|
32
|
+
// schema, so it lives here, in a file every validator loads, and the
|
|
33
|
+
// normalizers in draft7.js load only for a schema that declares one of these.
|
|
34
|
+
// draft-06 is read through the draft-07 rules (see draft7.js).
|
|
35
|
+
const DRAFT7_SCHEMAS = new Set([
|
|
36
|
+
'http://json-schema.org/draft-07/schema#',
|
|
37
|
+
'http://json-schema.org/draft-07/schema',
|
|
38
|
+
'http://json-schema.org/draft-06/schema#',
|
|
39
|
+
'http://json-schema.org/draft-06/schema',
|
|
40
|
+
]);
|
|
41
|
+
const DRAFT6_SCHEMAS = new Set([
|
|
42
|
+
'http://json-schema.org/draft-06/schema#',
|
|
43
|
+
'http://json-schema.org/draft-06/schema',
|
|
44
|
+
]);
|
|
45
|
+
// draft-04 is read through the draft-06 rules plus its own: `id` for `$id`,
|
|
46
|
+
// and no `const`, `contains` or `propertyNames` (see draft7.js).
|
|
47
|
+
const DRAFT4_SCHEMAS = new Set([
|
|
48
|
+
'http://json-schema.org/draft-04/schema#',
|
|
49
|
+
'http://json-schema.org/draft-04/schema',
|
|
50
|
+
]);
|
|
51
|
+
const DRAFT2019_SCHEMAS = new Set([
|
|
52
|
+
'https://json-schema.org/draft/2019-09/schema',
|
|
53
|
+
'https://json-schema.org/draft/2019-09/schema#',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// 0 for 2020-12 and later, 1 draft-07, 2 draft-06, 3 2019-09, 4 draft-04.
|
|
57
|
+
function draftCode(uri) {
|
|
58
|
+
if (typeof uri !== 'string') return 0;
|
|
59
|
+
if (DRAFT4_SCHEMAS.has(uri)) return 4;
|
|
60
|
+
if (DRAFT6_SCHEMAS.has(uri)) return 2;
|
|
61
|
+
if (DRAFT7_SCHEMAS.has(uri)) return 1;
|
|
62
|
+
if (DRAFT2019_SCHEMAS.has(uri)) return 3;
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { isV1Dialect, V1_DIALECTS, DRAFT7_SCHEMAS, DRAFT6_SCHEMAS, DRAFT4_SCHEMAS, DRAFT2019_SCHEMAS, draftCode };
|
package/lib/draft7.js
CHANGED
|
@@ -6,16 +6,7 @@
|
|
|
6
6
|
// this, a draft-06 schema was read under 2020-12 and its tuple `items`,
|
|
7
7
|
// `additionalItems` and `dependencies` were not applied: 30 cases of the
|
|
8
8
|
// official draft-06 suite accepted documents they must reject.
|
|
9
|
-
const DRAFT7_SCHEMAS =
|
|
10
|
-
'http://json-schema.org/draft-07/schema#',
|
|
11
|
-
'http://json-schema.org/draft-07/schema',
|
|
12
|
-
'http://json-schema.org/draft-06/schema#',
|
|
13
|
-
'http://json-schema.org/draft-06/schema',
|
|
14
|
-
])
|
|
15
|
-
const DRAFT6_SCHEMAS = new Set([
|
|
16
|
-
'http://json-schema.org/draft-06/schema#',
|
|
17
|
-
'http://json-schema.org/draft-06/schema',
|
|
18
|
-
])
|
|
9
|
+
const { DRAFT7_SCHEMAS, DRAFT6_SCHEMAS, DRAFT4_SCHEMAS, DRAFT2019_SCHEMAS, draftCode } = require('./dialect')
|
|
19
10
|
|
|
20
11
|
function isDraft7(schema) {
|
|
21
12
|
return !!(schema && typeof schema.$schema === 'string' && DRAFT7_SCHEMAS.has(schema.$schema))
|
|
@@ -28,9 +19,11 @@ function isDraft6(schema) {
|
|
|
28
19
|
// `force` applies the draft-07 rules to a document that declares no dialect
|
|
29
20
|
// of its own, which is how a retrieved schema inherits the root's draft.
|
|
30
21
|
// `draft6` says the document, or the root it inherits from, is draft-06.
|
|
31
|
-
|
|
22
|
+
// `draft4`: draft-04, read as draft-06 plus its own differences (below).
|
|
23
|
+
function normalizeDraft7(schema, force, draft6, draft4) {
|
|
32
24
|
if (!force && !isDraft7(schema)) return schema
|
|
33
|
-
|
|
25
|
+
const d4 = draft4 === true || (typeof schema.$schema === 'string' && DRAFT4_SCHEMAS.has(schema.$schema))
|
|
26
|
+
_normalize(schema, d4 || draft6 === true || isDraft6(schema), d4)
|
|
34
27
|
return schema
|
|
35
28
|
}
|
|
36
29
|
|
|
@@ -38,13 +31,21 @@ function normalizeDraft7(schema, force, draft6) {
|
|
|
38
31
|
// definitions are addressable by pointer, annotations are inert.
|
|
39
32
|
const REF_SIBLINGS_KEPT = new Set(['$ref', '$defs', 'definitions', '$schema', '$comment', 'title', 'description', 'examples', 'default', 'readOnly', 'writeOnly'])
|
|
40
33
|
|
|
41
|
-
function _normalize(schema, d6) {
|
|
34
|
+
function _normalize(schema, d6, d4) {
|
|
42
35
|
if (typeof schema !== 'object' || schema === null) return
|
|
43
36
|
|
|
44
37
|
// draft-06 has no conditional: `if`, `then` and `else` are unknown keywords
|
|
45
38
|
// there, so they are dropped rather than applied.
|
|
46
39
|
if (d6) { delete schema.if; delete schema.then; delete schema.else }
|
|
47
40
|
|
|
41
|
+
// draft-04 names a schema's identifier `id`, and has none of draft-06's
|
|
42
|
+
// `const`, `contains` or `propertyNames`: unknown keywords there.
|
|
43
|
+
if (d4) {
|
|
44
|
+
delete schema.const; delete schema.contains; delete schema.propertyNames
|
|
45
|
+
if (typeof schema.id === 'string' && schema.$id === undefined && typeof schema.$ref !== 'string') schema.$id = schema.id
|
|
46
|
+
delete schema.id
|
|
47
|
+
}
|
|
48
|
+
|
|
48
49
|
// In draft-07 an object with `$ref` is that reference and nothing else:
|
|
49
50
|
// sibling keywords, `$id` included, are ignored. Dropping them here gives
|
|
50
51
|
// every engine the same reading without each having to know the draft.
|
|
@@ -97,7 +98,7 @@ function _normalize(schema, d6) {
|
|
|
97
98
|
for (const k in m) {
|
|
98
99
|
if (!Object.prototype.hasOwnProperty.call(m, k)) continue
|
|
99
100
|
const v = m[k]
|
|
100
|
-
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
101
|
+
if (typeof v === 'object' && v !== null) _normalize(v, d6, d4)
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -108,7 +109,7 @@ function _normalize(schema, d6) {
|
|
|
108
109
|
if (Array.isArray(a)) {
|
|
109
110
|
for (let i = 0; i < a.length; i++) {
|
|
110
111
|
const v = a[i]
|
|
111
|
-
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
112
|
+
if (typeof v === 'object' && v !== null) _normalize(v, d6, d4)
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -116,7 +117,7 @@ function _normalize(schema, d6) {
|
|
|
116
117
|
// Recurse into single sub-schemas
|
|
117
118
|
for (const key of D7_SINGLE_SUBS) {
|
|
118
119
|
const v = schema[key]
|
|
119
|
-
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
120
|
+
if (typeof v === 'object' && v !== null) _normalize(v, d6, d4)
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
// The schema-bearing keywords _normalize walks, built once rather than per node.
|
|
@@ -313,10 +314,6 @@ function _stripFormat(schema, seen) {
|
|
|
313
314
|
// carries the anchor, and a plain `$ref: "#"` when it does not, which is what
|
|
314
315
|
// 2019-09 says it means then. The outward search for the outermost anchored
|
|
315
316
|
// resource is the same rule in both drafts.
|
|
316
|
-
const DRAFT2019_SCHEMAS = new Set([
|
|
317
|
-
'https://json-schema.org/draft/2019-09/schema',
|
|
318
|
-
'https://json-schema.org/draft/2019-09/schema#',
|
|
319
|
-
])
|
|
320
317
|
const RECURSIVE_ANCHOR = '__ata_recursive_anchor'
|
|
321
318
|
|
|
322
319
|
function isDraft2019(schema) {
|
|
@@ -368,12 +365,4 @@ function _normalize2019(node, resourceRoot, isRoot) {
|
|
|
368
365
|
// The draft a `$schema` value names, for the normalizers: 2 draft-06,
|
|
369
366
|
// 1 draft-07, 3 2019-09, 0 anything else. Takes the string so the caller
|
|
370
367
|
// reads the property once.
|
|
371
|
-
function draftCode(uri) {
|
|
372
|
-
if (typeof uri !== 'string') return 0
|
|
373
|
-
if (DRAFT6_SCHEMAS.has(uri)) return 2
|
|
374
|
-
if (DRAFT7_SCHEMAS.has(uri)) return 1
|
|
375
|
-
if (DRAFT2019_SCHEMAS.has(uri)) return 3
|
|
376
|
-
return 0
|
|
377
|
-
}
|
|
378
|
-
|
|
379
368
|
module.exports = { draftCode, isDraft7, isDraft6, isDraft2019, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions }
|
package/lib/enrich-site.js
CHANGED
|
@@ -16,9 +16,31 @@ const { reprValue, expectedFor, detailFor, rankFor, MISSING, DOC_BASE } = requir
|
|
|
16
16
|
// output. A module of its own because only the code generator reaches it, and
|
|
17
17
|
// ata-validator/lite, which enriches through `enrich`, should not carry it.
|
|
18
18
|
const NO_EXPECTED = {};
|
|
19
|
+
// `detail` (see detailFor in lib/enrich-error.js), worked out at the site as
|
|
20
|
+
// far as the site allows: the whole string when it depends only on the
|
|
21
|
+
// params, its fixed head when only the received value is left to append, and
|
|
22
|
+
// neither when it depends on the value (type) or the params are not fixed.
|
|
23
|
+
const DETAIL_AT_RUN = 0;
|
|
24
|
+
const DETAIL_FIXED = 1;
|
|
25
|
+
const DETAIL_HEAD = 2;
|
|
26
|
+
const RANGE_KEYWORDS = new Set(['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'minLength', 'maxLength']);
|
|
27
|
+
function siteDetail (keyword, params, expected) {
|
|
28
|
+
if (params === undefined || keyword === 'type') return [DETAIL_AT_RUN, undefined];
|
|
29
|
+
switch (keyword) {
|
|
30
|
+
case 'required': return [DETAIL_FIXED, `missing required property "${params.missingProperty}"`];
|
|
31
|
+
case 'additionalProperties': return [DETAIL_FIXED, `unknown property "${params.additionalProperty}"`];
|
|
32
|
+
case 'unevaluatedProperties': return [DETAIL_FIXED, `unevaluated property "${params.unevaluatedProperty}"`];
|
|
33
|
+
case 'format': return [DETAIL_HEAD, `not a valid ${params.format}: `];
|
|
34
|
+
}
|
|
35
|
+
if (RANGE_KEYWORDS.has(keyword) || expected) return [DETAIL_HEAD, `expected ${expected}, found `];
|
|
36
|
+
return [DETAIL_FIXED, undefined];
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
function errorSite (keyword, schemaPath, params, format) {
|
|
20
40
|
const dependent = keyword === 'required' && typeof schemaPath === 'string' && schemaPath.endsWith('/dependentRequired');
|
|
21
41
|
const code = (dependent && 'ATA7005') || codeFor(keyword, format) || 'ATA9001';
|
|
42
|
+
const expected = params === undefined ? NO_EXPECTED : expectedFor({ keyword, params });
|
|
43
|
+
const [detailKind, detail] = siteDetail(keyword, params, expected);
|
|
22
44
|
return {
|
|
23
45
|
code,
|
|
24
46
|
keyword,
|
|
@@ -26,7 +48,9 @@ function errorSite (keyword, schemaPath, params, format) {
|
|
|
26
48
|
docUrl: DOC_BASE + code,
|
|
27
49
|
rank: rankFor(keyword),
|
|
28
50
|
headline: (CODES[code] && CODES[code].headline) || 'validation failed',
|
|
29
|
-
expected
|
|
51
|
+
expected,
|
|
52
|
+
detailKind,
|
|
53
|
+
detail,
|
|
30
54
|
};
|
|
31
55
|
}
|
|
32
56
|
|
|
@@ -49,7 +73,10 @@ function makeRich (site, path, params, message, value, root, spPrefix) {
|
|
|
49
73
|
};
|
|
50
74
|
const sugg = suggestFor(out, root, at === MISSING ? undefined : at);
|
|
51
75
|
if (sugg) out.suggestion = sugg;
|
|
52
|
-
const
|
|
76
|
+
const kind = site.detailKind;
|
|
77
|
+
const detail = kind === DETAIL_FIXED ? site.detail
|
|
78
|
+
: kind === DETAIL_HEAD ? site.detail + out.received
|
|
79
|
+
: detailFor({ keyword: site.keyword, params }, out, at);
|
|
53
80
|
if (detail !== undefined) out.detail = detail;
|
|
54
81
|
out.rank = site.rank;
|
|
55
82
|
return out;
|
package/lib/error-runtime.js
CHANGED
|
@@ -23,6 +23,8 @@ const FILES = [
|
|
|
23
23
|
'safe-regex',
|
|
24
24
|
'unique-items',
|
|
25
25
|
'formats',
|
|
26
|
+
// The URI walk formats.js loads; left out with formats (see OPTIONAL).
|
|
27
|
+
'uri-core',
|
|
26
28
|
];
|
|
27
29
|
// The parts a build can leave out when none of its schemas needs them. The
|
|
28
30
|
// decision is made on the schemas the runtime walks (as embedded, see
|
|
@@ -91,8 +93,10 @@ function errorRuntimeSource(format, version, features, schemaMode) {
|
|
|
91
93
|
if (hit !== undefined) return hit;
|
|
92
94
|
const defs = [];
|
|
93
95
|
for (const name of schemaMode ? FILES.concat(SCHEMA_FILES) : FILES) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
// uri-core goes with formats: only formats.js requires it.
|
|
97
|
+
const part = name === 'uri-core' ? 'formats' : name;
|
|
98
|
+
const src = OPTIONAL[part] !== undefined && !want(part)
|
|
99
|
+
? (name === 'uri-core' ? 'module.exports = {};' : OPTIONAL[part].stub)
|
|
96
100
|
: dropCommentLines(fs.readFileSync(path.join(__dirname, name + '.js'), 'utf8'), name);
|
|
97
101
|
// The files' own require('./x') calls go to the table under another name:
|
|
98
102
|
// a tool that reads the runtime without scope (knip, a lint rule) took a
|
|
@@ -130,6 +134,7 @@ function _ataCode(k, f) {
|
|
|
130
134
|
// The modules are evaluated on the first call, not when this file loads.
|
|
131
135
|
function createErrors(schema, options) { return _ataRequire('error-runtime-entry').createErrors(schema, _ataCode, options); }
|
|
132
136
|
function compileSafe(src) { return _ataRequire('safe-regex').compileSafe(src); }
|
|
137
|
+
function uriWalk(s, m) { return _ataRequire('formats').uriWalk(s, m); }
|
|
133
138
|
${schemaMode ? "function createValidator(schema, options) { return _ataRequire('schema-runtime-entry').createValidator(schema, _ataCode, options); }\n" : ''}
|
|
134
139
|
const ataVersion = ${JSON.stringify(version)};
|
|
135
140
|
|
|
@@ -147,8 +152,8 @@ if (typeof requestIdleCallback === 'function') { try { requestIdleCallback(warm)
|
|
|
147
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 (_) {} }
|
|
148
153
|
`;
|
|
149
154
|
const out = format === 'cjs'
|
|
150
|
-
? body + `module.exports = { createErrors, compileSafe, ${schemaMode ? 'createValidator, ' : ''}ataVersion };\n`
|
|
151
|
-
: 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`;
|
|
152
157
|
_cache.set(key, out);
|
|
153
158
|
return out;
|
|
154
159
|
}
|