ata-validator 1.7.4 → 1.8.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/CHANGELOG.md +14 -0
- package/README.md +18 -6
- package/index.js +52 -1
- package/lib/aot.js +13 -3
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +199 -0
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 1.8.0 - 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `$vocabulary` is honoured when a schema names a custom meta-schema in `$schema`. A dialect is a set of vocabularies and a vocabulary is a set of keywords, so a keyword whose vocabulary the meta-schema does not declare is not part of that dialect at all: it is an unknown keyword, and an unknown keyword is ignored. A schema written against a meta-schema which declares only the core and applicator vocabularies now has its `minimum`, `type` and the rest of the validation keywords ignored, while `properties` and the other applicators still apply. The keywords belonging to each vocabulary are read from the vendored meta-schemas rather than from a list kept by hand, so they come from the specification. Removing the keyword before compilation means every engine agrees without any of them knowing what a vocabulary is.
|
|
10
|
+
|
|
11
|
+
This closes the last case ata missed. Draft 2020-12 goes from 1298 to **1299 of 1299**, with draft 7 at 927 of 927 and the v1 dialect at 1133 of 1133, the same with code generation blocked. `tests/run_suite.js` has no known failures left.
|
|
12
|
+
|
|
13
|
+
Two things it does not do. It does not refuse a schema whose meta-schema requires, with `true`, a vocabulary ata does not recognise; the specification says an implementation must refuse there, and turning what has always been silently accepted into a hard failure is a separate decision, so such a schema is evaluated as before with every keyword applied. And a separate document reached through `$ref` keeps its own keywords rather than inheriting the referring dialect, so a document which should follow one needs to say so with its own `$schema`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `bundleStandalone` and `bundleCompact` built the error-reporting function from the schema the caller passed rather than the one the validator compiled. Anything which prepares a schema therefore reached the boolean path and not the error path: with `assertFormat: false` the bundle reported a `format` error the caller had turned off, and only once some other keyword failed first, since the error function runs only after the boolean one says no. `toStandalone` already read the compiled schema; these two now do as well.
|
|
18
|
+
|
|
5
19
|
## 1.7.4 - 2026-08-26
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -14,17 +14,29 @@ npm install --save-dev ata-validator
|
|
|
14
14
|
npx ata build 'schemas/*.json' --out-dir src/generated
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. For a guaranteed zero-binary install:
|
|
17
|
+
The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npm install ata-validator --omit=optional
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one,
|
|
23
|
+
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1299 of 1299 Draft 2020-12 cases. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
|
|
24
24
|
|
|
25
25
|
Those four now agree with `validate()` on every case of the official suite, 3359 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
|
|
26
26
|
|
|
27
|
-
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same
|
|
27
|
+
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1299 of 1299 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
|
|
28
|
+
|
|
29
|
+
Node has a switch for exactly that environment, so this takes thirty seconds to check for yourself, on ata or on whatever you use today:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node --disallow-code-generation-from-strings -e "
|
|
33
|
+
const { Validator } = require('ata-validator')
|
|
34
|
+
const v = new Validator({ type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] })
|
|
35
|
+
console.log(v.validate({ id: 1 }).valid, v.validate({ id: 'x' }).valid)
|
|
36
|
+
"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
A validator that reaches its speed by generating source and calling `new Function` cannot run under that flag at all, which is the same reason it cannot run on a Worker. That is a deliberate trade rather than an oversight, and it is worth knowing which side of it your validator is on before you deploy to an edge runtime.
|
|
28
40
|
|
|
29
41
|
In your code:
|
|
30
42
|
|
|
@@ -428,7 +440,7 @@ const result = v['~standard'].validate(data);
|
|
|
428
440
|
|
|
429
441
|
### Fastify Plugin
|
|
430
442
|
|
|
431
|
-
Measured against Fastify's own schema and validation test files with ata swapped in as the default validator:
|
|
443
|
+
Measured against Fastify's own schema and validation test files with ata swapped in as the default validator: 178 of 184 tests pass, and the remaining six assert the default validator's own extension API rather than validation behavior. Validation errors follow the schema's keyword declaration order, so error-message plugins and `transformErrors` hooks written against the default validator see the same order.
|
|
432
444
|
|
|
433
445
|
```bash
|
|
434
446
|
npm install fastify-ata
|
|
@@ -507,12 +519,12 @@ Both are implemented in the interpreted engine, so a v1 schema that uses `$dynam
|
|
|
507
519
|
|
|
508
520
|
### Known limitations
|
|
509
521
|
|
|
510
|
-
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives
|
|
522
|
+
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1299 of 1299 cases. Draft 7 gives 927 of 927. The v1 dialect gives 1133 of 1133. `npm run test:suite` reproduces all three.
|
|
511
523
|
|
|
512
524
|
Areas that remain deliberate scope decisions for 1.x:
|
|
513
525
|
|
|
514
526
|
- **Remote `$ref` over the network** is not fetched. Register cross-schema refs explicitly with `schemas: [...]` or `addSchema()`.
|
|
515
|
-
- **`$vocabulary`** is not
|
|
527
|
+
- **`$vocabulary`** is honoured for the document which names the meta-schema: a keyword whose vocabulary that meta-schema does not declare is not part of the dialect, so it is treated as unknown and does not apply. Two things it does not do. It does not refuse a schema whose meta-schema requires a vocabulary ata does not recognise, which the specification says an implementation must do; that schema is evaluated with every keyword applied, as before. And a separate document reached through `$ref` keeps its own keywords rather than inheriting the referring dialect, so register it with its own `$schema` if it should follow one.
|
|
516
528
|
- **`contentEncoding` / `contentMediaType` / `contentSchema`** are annotation-only, as the spec permits, and are not validated.
|
|
517
529
|
- **`minLength`/`maxLength`** count UTF-16 code units, not grapheme clusters.
|
|
518
530
|
- **Infinite-loop detection** relies on a recursion depth guard that cuts off circular `$ref` chains.
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
compileToJSCombined,
|
|
11
11
|
} = require("./lib/js-compiler");
|
|
12
12
|
const { normalizeDraft7, normalizeNullable, stripFormatAssertions } = require("./lib/draft7");
|
|
13
|
+
const { enabledKeywords, stripDisabledKeywords } = require("./lib/vocabularies");
|
|
13
14
|
const { isV1Dialect } = require("./lib/dialect");
|
|
14
15
|
const { classify } = require("./lib/shape-classifier");
|
|
15
16
|
const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
|
|
@@ -511,6 +512,29 @@ function buildSchemaMap(schemas, inheritDraft7) {
|
|
|
511
512
|
return map
|
|
512
513
|
}
|
|
513
514
|
|
|
515
|
+
// A schema which names a custom meta-schema in `$schema` is written against
|
|
516
|
+
// whatever dialect that meta-schema declares. A keyword from a vocabulary the
|
|
517
|
+
// dialect does not have is not part of the dialect, so it is an unknown
|
|
518
|
+
// keyword and does not apply. Removing it here means every engine sees the
|
|
519
|
+
// same schema and none of them needs to know about vocabularies.
|
|
520
|
+
//
|
|
521
|
+
// Only the root is consulted. A subschema naming its own `$schema` is its own
|
|
522
|
+
// resource under its own dialect, and the walk stops there rather than
|
|
523
|
+
// applying this dialect's answer to it.
|
|
524
|
+
function _applyVocabularies(schemaObj, original, schemaMap) {
|
|
525
|
+
if (!schemaObj || typeof schemaObj !== 'object') return schemaObj
|
|
526
|
+
const declared = schemaObj.$schema
|
|
527
|
+
if (typeof declared !== 'string') return schemaObj
|
|
528
|
+
const enabled = enabledKeywords(schemaMap.get(declared))
|
|
529
|
+
if (!enabled) return schemaObj
|
|
530
|
+
// `original` is the caller's own object when it reached here unchanged, and
|
|
531
|
+
// that one is never mutated.
|
|
532
|
+
const copy = schemaObj === original
|
|
533
|
+
? _deepCloneWithSymbols(schemaObj)
|
|
534
|
+
: schemaObj
|
|
535
|
+
return stripDisabledKeywords(copy, enabled)
|
|
536
|
+
}
|
|
537
|
+
|
|
514
538
|
// Compile-cache key for a root schema plus its external schemas. Must include
|
|
515
539
|
// the external schema CONTENT, not just their $ids: two validators can share a
|
|
516
540
|
// root schema string and the same $id while pointing that $id at different
|
|
@@ -621,6 +645,13 @@ class Validator {
|
|
|
621
645
|
);
|
|
622
646
|
}
|
|
623
647
|
|
|
648
|
+
// Built here rather than below because `$vocabulary` is resolved against
|
|
649
|
+
// it, and that resolution waits until compilation so a meta-schema
|
|
650
|
+
// registered by addSchema() still counts.
|
|
651
|
+
const schemaMap = buildSchemaMap(options.schemas, rootIsDraft7) || new Map();
|
|
652
|
+
this._schemaIsCallers = schemaObj === schema;
|
|
653
|
+
this._vocabulariesApplied = false;
|
|
654
|
+
|
|
624
655
|
this._schemaStr = null; // lazy: computed on first use
|
|
625
656
|
this._schemaObj = schemaObj;
|
|
626
657
|
this._options = options;
|
|
@@ -634,7 +665,7 @@ class Validator {
|
|
|
634
665
|
this._applyDefaults = null;
|
|
635
666
|
|
|
636
667
|
// Schema map for cross-schema $ref resolution
|
|
637
|
-
this._schemaMap =
|
|
668
|
+
this._schemaMap = schemaMap;
|
|
638
669
|
|
|
639
670
|
// User-supplied format checkers: { formatName: (value) => boolean }.
|
|
640
671
|
// Looked up at runtime when a schema references a format the built-in
|
|
@@ -758,8 +789,27 @@ class Validator {
|
|
|
758
789
|
}
|
|
759
790
|
}
|
|
760
791
|
|
|
792
|
+
// `$vocabulary` says which keywords the dialect has, and answering needs the
|
|
793
|
+
// meta-schema, which addSchema() may only have registered just now. Run once,
|
|
794
|
+
// before anything reads the schema, and before `_schemaStr` is computed from
|
|
795
|
+
// it. After this addSchema() is refused, so the answer cannot go stale.
|
|
796
|
+
_ensureVocabularies() {
|
|
797
|
+
if (this._vocabulariesApplied) return;
|
|
798
|
+
this._vocabulariesApplied = true;
|
|
799
|
+
const stripped = _applyVocabularies(
|
|
800
|
+
this._schemaObj,
|
|
801
|
+
this._schemaIsCallers ? this._schemaObj : null,
|
|
802
|
+
this._schemaMap,
|
|
803
|
+
);
|
|
804
|
+
if (stripped !== this._schemaObj) {
|
|
805
|
+
this._schemaObj = stripped;
|
|
806
|
+
this._schemaStr = null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
761
810
|
_ensureCompiled() {
|
|
762
811
|
if (this._initialized) return;
|
|
812
|
+
this._ensureVocabularies();
|
|
763
813
|
this._initialized = true;
|
|
764
814
|
|
|
765
815
|
const schemaObj = this._schemaObj;
|
|
@@ -1487,6 +1537,7 @@ class Validator {
|
|
|
1487
1537
|
|
|
1488
1538
|
_ensureCodegen() {
|
|
1489
1539
|
if (this._jsFn) return;
|
|
1540
|
+
this._ensureVocabularies();
|
|
1490
1541
|
if (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI) return;
|
|
1491
1542
|
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
1492
1543
|
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
package/lib/aot.js
CHANGED
|
@@ -285,8 +285,13 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
285
285
|
v._ensureCompiled();
|
|
286
286
|
const jsFn = v._jsFn;
|
|
287
287
|
if (!jsFn || !jsFn._hybridSource) return 'null';
|
|
288
|
+
// The schema the validator compiled, not the one the caller passed. They
|
|
289
|
+
// differ whenever anything prepared the schema: draft-07 normalization,
|
|
290
|
+
// `format` removed under assertFormat: false, keywords outside the
|
|
291
|
+
// dialect's vocabularies. Reading the original here would leave the
|
|
292
|
+
// boolean path and the error path disagreeing about the same document.
|
|
288
293
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
289
|
-
|
|
294
|
+
v._schemaObj,
|
|
290
295
|
v._schemaMap,
|
|
291
296
|
v._userFormats,
|
|
292
297
|
);
|
|
@@ -311,7 +316,7 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
311
316
|
}
|
|
312
317
|
if (opts && opts.verbose) {
|
|
313
318
|
// Embed the schema and a small resolver so errors carry parentSchema.
|
|
314
|
-
const schemaLit = JSON.stringify(
|
|
319
|
+
const schemaLit = JSON.stringify(v._schemaObj);
|
|
315
320
|
return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}var E=function(d){var _all=true;${errBody}};var _v=function(d){${jsFn._hybridSource}};return function(d){var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r}})(R)`;
|
|
316
321
|
}
|
|
317
322
|
return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
@@ -343,8 +348,13 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
343
348
|
v._ensureCompiled();
|
|
344
349
|
const jsFn = v._jsFn;
|
|
345
350
|
if (!jsFn || !jsFn._hybridSource) return null;
|
|
351
|
+
// The schema the validator compiled, not the one the caller passed. They
|
|
352
|
+
// differ whenever anything prepared the schema: draft-07 normalization,
|
|
353
|
+
// `format` removed under assertFormat: false, keywords outside the
|
|
354
|
+
// dialect's vocabularies. Reading the original here would leave the
|
|
355
|
+
// boolean path and the error path disagreeing about the same document.
|
|
346
356
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
347
|
-
|
|
357
|
+
v._schemaObj,
|
|
348
358
|
v._schemaMap,
|
|
349
359
|
v._userFormats,
|
|
350
360
|
);
|
package/lib/version.js
CHANGED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `$vocabulary`, as far as evaluation is concerned.
|
|
4
|
+
//
|
|
5
|
+
// A dialect is a set of vocabularies, and a vocabulary is a set of keywords.
|
|
6
|
+
// When a schema names a custom meta-schema in `$schema`, that meta-schema's
|
|
7
|
+
// `$vocabulary` says which vocabularies the dialect has. A keyword whose
|
|
8
|
+
// vocabulary is not there is not part of the dialect at all, so it is an
|
|
9
|
+
// unknown keyword, and an unknown keyword is ignored. Deleting it is exactly
|
|
10
|
+
// that reading, and it means every engine gets this for free rather than each
|
|
11
|
+
// of them growing a notion of vocabularies.
|
|
12
|
+
//
|
|
13
|
+
// The keyword lists are not written out here. Each official vocabulary has a
|
|
14
|
+
// meta-schema whose `properties` are precisely that vocabulary's keywords,
|
|
15
|
+
// and those documents are already vendored in `metaschemas.js`, so the lists
|
|
16
|
+
// come from the specification rather than from someone keeping a copy in
|
|
17
|
+
// step with it.
|
|
18
|
+
//
|
|
19
|
+
// What this deliberately does not do: refuse a schema whose meta-schema
|
|
20
|
+
// requires a vocabulary ata does not know. The specification says an
|
|
21
|
+
// implementation must refuse there, and ata does not, because until now it
|
|
22
|
+
// ignored `$vocabulary` entirely and turning that into a hard failure would
|
|
23
|
+
// break schemas which work today. Such a schema is left exactly as it was,
|
|
24
|
+
// evaluated with every keyword applied. See `docs/` for the limitation.
|
|
25
|
+
|
|
26
|
+
const { METASCHEMAS } = require('./metaschemas')
|
|
27
|
+
|
|
28
|
+
const CORE_VOCABULARY = 'https://json-schema.org/draft/2020-12/vocab/core'
|
|
29
|
+
|
|
30
|
+
// vocabulary URI -> Set of the keywords it defines, built once on first use.
|
|
31
|
+
let KEYWORDS_BY_VOCABULARY = null
|
|
32
|
+
|
|
33
|
+
function keywordsByVocabulary() {
|
|
34
|
+
if (KEYWORDS_BY_VOCABULARY) return KEYWORDS_BY_VOCABULARY
|
|
35
|
+
const byVocabulary = new Map()
|
|
36
|
+
for (const name of [
|
|
37
|
+
'core',
|
|
38
|
+
'applicator',
|
|
39
|
+
'unevaluated',
|
|
40
|
+
'validation',
|
|
41
|
+
'meta-data',
|
|
42
|
+
'format-annotation',
|
|
43
|
+
'content',
|
|
44
|
+
]) {
|
|
45
|
+
const meta = METASCHEMAS.get(
|
|
46
|
+
`https://json-schema.org/draft/2020-12/meta/${name}`,
|
|
47
|
+
)
|
|
48
|
+
if (!meta || !meta.properties) continue
|
|
49
|
+
byVocabulary.set(
|
|
50
|
+
`https://json-schema.org/draft/2020-12/vocab/${name}`,
|
|
51
|
+
new Set(Object.keys(meta.properties)),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
// The one vocabulary with no document to read. `format-assertion` is an
|
|
55
|
+
// alternative to `format-annotation` rather than part of the standard
|
|
56
|
+
// dialect, so its meta-schema is not among the vendored ones. It defines
|
|
57
|
+
// the single keyword `format`, which is what the other one defines, and a
|
|
58
|
+
// vocabulary's keyword set does not change once it is published. Without
|
|
59
|
+
// this a meta-schema asking for format assertion, which is the standard
|
|
60
|
+
// way to ask, would be a vocabulary ata does not know and no filtering
|
|
61
|
+
// would happen at all.
|
|
62
|
+
byVocabulary.set(
|
|
63
|
+
'https://json-schema.org/draft/2020-12/vocab/format-assertion',
|
|
64
|
+
new Set(['format']),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
KEYWORDS_BY_VOCABULARY = byVocabulary
|
|
68
|
+
return byVocabulary
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Every keyword ata can attribute to a vocabulary. A keyword outside this set
|
|
72
|
+
// belongs to no vocabulary ata knows, so `$vocabulary` says nothing about it
|
|
73
|
+
// and it is left alone.
|
|
74
|
+
function allVocabularyKeywords() {
|
|
75
|
+
const all = new Set()
|
|
76
|
+
for (const keywords of keywordsByVocabulary().values()) {
|
|
77
|
+
for (const keyword of keywords) all.add(keyword)
|
|
78
|
+
}
|
|
79
|
+
return all
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Given a meta-schema, the keywords a schema written against it may use, or
|
|
83
|
+
// `null` when the question does not arise or cannot be answered:
|
|
84
|
+
//
|
|
85
|
+
// - the document declares no `$vocabulary`, so it does not describe a
|
|
86
|
+
// dialect in these terms and every keyword stands
|
|
87
|
+
// - it requires a vocabulary ata does not know, so ata cannot say which
|
|
88
|
+
// keywords the dialect has and does not guess
|
|
89
|
+
function enabledKeywords(metaschema) {
|
|
90
|
+
if (!metaschema || typeof metaschema !== 'object') return null
|
|
91
|
+
const vocabularies = metaschema.$vocabulary
|
|
92
|
+
if (!vocabularies || typeof vocabularies !== 'object') return null
|
|
93
|
+
|
|
94
|
+
const known = keywordsByVocabulary()
|
|
95
|
+
const enabled = new Set()
|
|
96
|
+
for (const [uri, required] of Object.entries(vocabularies)) {
|
|
97
|
+
const keywords = known.get(uri)
|
|
98
|
+
if (keywords) {
|
|
99
|
+
for (const keyword of keywords) enabled.add(keyword)
|
|
100
|
+
} else if (required === true) {
|
|
101
|
+
return null // a required vocabulary ata cannot account for
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Core is what makes a document a schema at all: `$ref`, `$defs`, `$id`.
|
|
106
|
+
// A meta-schema which omits it is not describing something ata could
|
|
107
|
+
// evaluate, so treat core as present rather than stripping the plumbing.
|
|
108
|
+
for (const keyword of known.get(CORE_VOCABULARY) || []) enabled.add(keyword)
|
|
109
|
+
|
|
110
|
+
// The usual case is a meta-schema which has every vocabulary, and then
|
|
111
|
+
// there is nothing to answer. Saying so here keeps the caller from copying
|
|
112
|
+
// a schema it would not have changed.
|
|
113
|
+
for (const keyword of allVocabularyKeywords()) {
|
|
114
|
+
if (!enabled.has(keyword)) return enabled
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Delete every keyword which belongs to a vocabulary the dialect does not
|
|
120
|
+
// have. Mutates, so callers pass a copy.
|
|
121
|
+
function stripDisabledKeywords(schema, enabled) {
|
|
122
|
+
const removable = allVocabularyKeywords()
|
|
123
|
+
const disabled = new Set()
|
|
124
|
+
for (const keyword of removable) {
|
|
125
|
+
if (!enabled.has(keyword)) disabled.add(keyword)
|
|
126
|
+
}
|
|
127
|
+
if (disabled.size === 0) return schema
|
|
128
|
+
_strip(schema, disabled, new Set(), true)
|
|
129
|
+
return schema
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _strip(schema, disabled, seen, isRoot) {
|
|
133
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
134
|
+
if (Array.isArray(schema)) {
|
|
135
|
+
for (const each of schema) _strip(each, disabled, seen, false)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (seen.has(schema)) return
|
|
139
|
+
seen.add(schema)
|
|
140
|
+
|
|
141
|
+
// A subschema which names its own `$schema` is its own resource under its
|
|
142
|
+
// own dialect, so this dialect's vocabularies say nothing about it. The
|
|
143
|
+
// root names one by definition, which is how this dialect was chosen.
|
|
144
|
+
if (!isRoot && typeof schema.$schema === 'string') return
|
|
145
|
+
|
|
146
|
+
for (const keyword of disabled) {
|
|
147
|
+
if (keyword in schema) delete schema[keyword]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Walk what is left. A disabled applicator has already been deleted, so
|
|
151
|
+
// this only descends through subschemas which still apply.
|
|
152
|
+
const objSubs = [
|
|
153
|
+
'properties',
|
|
154
|
+
'patternProperties',
|
|
155
|
+
'$defs',
|
|
156
|
+
'definitions',
|
|
157
|
+
'dependentSchemas',
|
|
158
|
+
]
|
|
159
|
+
for (const key of objSubs) {
|
|
160
|
+
const box = schema[key]
|
|
161
|
+
if (box && typeof box === 'object' && !Array.isArray(box)) {
|
|
162
|
+
for (const each of Object.values(box)) _strip(each, disabled, seen, false)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
|
|
166
|
+
for (const key of arrSubs) {
|
|
167
|
+
if (Array.isArray(schema[key])) {
|
|
168
|
+
for (const each of schema[key]) _strip(each, disabled, seen, false)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const singleSubs = [
|
|
172
|
+
'items',
|
|
173
|
+
'additionalItems',
|
|
174
|
+
'contains',
|
|
175
|
+
'not',
|
|
176
|
+
'if',
|
|
177
|
+
'then',
|
|
178
|
+
'else',
|
|
179
|
+
'additionalProperties',
|
|
180
|
+
'propertyNames',
|
|
181
|
+
'unevaluatedItems',
|
|
182
|
+
'unevaluatedProperties',
|
|
183
|
+
'contentSchema',
|
|
184
|
+
]
|
|
185
|
+
for (const key of singleSubs) {
|
|
186
|
+
const sub = schema[key]
|
|
187
|
+
if (Array.isArray(sub)) {
|
|
188
|
+
for (const each of sub) _strip(each, disabled, seen, false)
|
|
189
|
+
} else {
|
|
190
|
+
_strip(sub, disabled, seen, false)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
enabledKeywords,
|
|
197
|
+
stripDisabledKeywords,
|
|
198
|
+
keywordsByVocabulary,
|
|
199
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
|
|
45
45
|
"build": "cmake-js build --target ata",
|
|
46
46
|
"rebuild": "cmake-js rebuild --target ata",
|
|
47
|
-
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_value_equality.js && node tests/test_lazy_errors.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
47
|
+
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_lazy_errors.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
48
48
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
49
49
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
50
50
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -112,13 +112,13 @@
|
|
|
112
112
|
"LICENSE"
|
|
113
113
|
],
|
|
114
114
|
"optionalDependencies": {
|
|
115
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
116
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
117
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
118
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
119
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
120
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
121
|
-
"@ata-validator/native-win32-x64": "1.
|
|
115
|
+
"@ata-validator/native-darwin-arm64": "1.8.0",
|
|
116
|
+
"@ata-validator/native-darwin-x64": "1.8.0",
|
|
117
|
+
"@ata-validator/native-linux-x64-gnu": "1.8.0",
|
|
118
|
+
"@ata-validator/native-linux-arm64-gnu": "1.8.0",
|
|
119
|
+
"@ata-validator/native-linux-x64-musl": "1.8.0",
|
|
120
|
+
"@ata-validator/native-linux-arm64-musl": "1.8.0",
|
|
121
|
+
"@ata-validator/native-win32-x64": "1.8.0"
|
|
122
122
|
},
|
|
123
123
|
"peerDependencies": {
|
|
124
124
|
"yaml": "^2.0.0"
|