ata-validator 1.8.0 → 1.8.2
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 +27 -1
- package/index.js +103 -4
- package/lib/js-compiler.js +11 -3
- package/lib/schema-scan.js +115 -0
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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.2 - 2026-08-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A `pattern` of the form `^[class]+$`, `^[class]{m,n}$` or `^[class]{n}$` with n above 16 could be violated without `validate()` saying so. The boolean program compiles those patterns to an inline character loop wrapped in an arrow function, and the rewrite that turns the boolean program into the error-reporting one replaced the `return false` and `return true` inside that arrow with `return E(d)` and `return R`. Both are objects, so the arrow started returning a truthy value on mismatch, the enclosing `!(cond && obj)` went false, and the error program accepted what the boolean program had rejected.
|
|
10
|
+
|
|
11
|
+
What that looked like from outside depended on the path. Without preprocessing, `validate()` still rejected, because the boolean verdict runs first, but the error it produced was a generic `schema validation failed` with no keyword, no `params.pattern` and no path. With preprocessing, which means any schema carrying a `default` or a validator built with `coerceTypes` or `removeAdditional`, the error program is installed as `validate()` directly and the result was `valid: true`. `Validator.bundleStandalone` and `bundleCompact` embed the same program and had the same silent accept. `ata compile` output is built from the boolean program and was not affected. The rewrite has been wrong since 0.6.0; the official suite never exercised these pattern shapes, so nothing caught it.
|
|
12
|
+
|
|
13
|
+
The rewrite now leaves the body of an arrow function alone, the same way it already left `function` bodies alone. `tests/test_hybrid_agreement.js` compares the rewritten program with the boolean it came from on every affected shape, at the root, in a property, in array items, through `bundleStandalone` and through a compiled module, and checks the two preprocessing cases directly. `tests/test_codegen_entrypoint_agreement.js` now compares the rewritten program as a fifth participant across the whole official suite. `tests/test_ajv_errors.js`, which had been failing on exactly this for as long as the bug existed, is now part of `npm test`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Ranking an error into schema declaration order no longer re-derives the rank on every error of every failing document. It was splitting the schema path with two regular expressions per segment and then scanning `Object.keys(node)` at each level, which was 8.5% of the error path in a profile, for an answer that depends only on the schema and the path. The rank is now cached per path under its root, each node's keys are indexed once, and escape handling is skipped for segments with no tilde. That function drops from 8.5% to 1.4% of the error path, and the error path as a whole gets 3.5% faster, median of eleven interleaved runs. `docs/performance-notes.md` records the larger gap this was measured against; it is not closed by this change.
|
|
18
|
+
|
|
19
|
+
## 1.8.1 - 2026-08-27
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Constructing a validator no longer clones and serializes every schema to find out whether it needed normalizing. It did that on the root and on each registered schema: serialize, deep clone, normalize the clone, serialize again, compare. For a schema with no `nullable` field and no draft-07 keyword the answer is always no, and the work was thrown away. Every question the normalizers ask is whether some key appears anywhere in the tree, so one walk answers all of them, and the clone now happens only for schemas that need it. No registry and ten fields goes from 18.5 to 4.6 µs; fifty registered schemas of fifty fields, which is a small server's worth, from 2570 to 260 µs.
|
|
24
|
+
|
|
25
|
+
The walk descends through every object-valued key rather than the subschema keywords the normalizers recurse through, so it is a superset of what they visit: it can report work where there is none, costing one clone, and cannot miss work there is. Across the 2344 schemas in the official suite it reports work on 335 where normalization changes 334. `tests/test_schema_scan.js` asserts that direction over the whole suite, and asserts the check is load-bearing by breaking the scan deliberately and confirming the broken one is caught.
|
|
26
|
+
|
|
27
|
+
The answer is remembered against the schema object, the way whole compiled validators already are, and so is the schema map built from a registry. Both were being redone once per validator, so a server building one validator per route over a shared registry paid for that registry once per route. Fifty routes over twenty shared schemas boots in 0.046 ms rather than 20.66 ms. Validators share the map, and anything that writes to one, which is `addSchema()` and the meta-schema registration during compilation, takes a private copy first.
|
|
28
|
+
|
|
29
|
+
Nothing about what any schema validates to changes. The comparison that decided the old answer is still there behind the walk, so a schema the walk sends down the slow path gets exactly the result it got before.
|
|
30
|
+
|
|
5
31
|
## 1.8.0 - 2026-08-27
|
|
6
32
|
|
|
7
33
|
### Added
|
|
@@ -24,7 +50,7 @@ All notable changes to ata-validator are documented here. The format follows [Ke
|
|
|
24
50
|
|
|
25
51
|
### Changed
|
|
26
52
|
|
|
27
|
-
- Rejecting a document through the buffer APIs no longer costs more than accepting one. The On-Demand plan answers first, and a `false` from it used to be ambiguous: it could mean the document failed a constraint, or that the plan could not decide. The caller had to assume the second, so it re-created the padded view, parsed the whole document again into a DOM, tried the generated plan, and then walked the tree. A rejected document was therefore parsed twice and walked up to three times. The plan now reports whether it stopped because the document failed a constraint or because it could not be read, and only the second falls through. simdjson's `INCORRECT_TYPE` is a constraint failure rather than a read failure, which is the distinction that makes this work: a property holding a string where the schema wants an integer surfaces as a read error from `get<int64>`, not as a type mismatch. On a 62 KB array of a thousand objects with one bad element: 316 µs to 11 µs when the bad element is first, 315 µs to 40 µs when it is last
|
|
53
|
+
- Rejecting a document through the buffer APIs no longer costs more than accepting one. The On-Demand plan answers first, and a `false` from it used to be ambiguous: it could mean the document failed a constraint, or that the plan could not decide. The caller had to assume the second, so it re-created the padded view, parsed the whole document again into a DOM, tried the generated plan, and then walked the tree. A rejected document was therefore parsed twice and walked up to three times. The plan now reports whether it stopped because the document failed a constraint or because it could not be read, and only the second falls through. simdjson's `INCORRECT_TYPE` is a constraint failure rather than a read failure, which is the distinction that makes this work: a property holding a string where the schema wants an integer surfaces as a read error from `get<int64>`, not as a type mismatch. On a 62 KB array of a thousand objects with one bad element: 316 µs to 11 µs when the bad element is first, 315 µs to 40 µs when it is last. On that shape rejecting is no longer dearer than accepting, and when the bad element is early it is several times cheaper. It is still dearer on a small document, where there is no bulk of parsing for an early exit to save: on a two-field object, 129 ns to accept and 278 ns to reject. `tests/test_buffer_path_parity.js` holds the buffer path to the same answers as `validate()` across all 3359 suite cases, and still reports zero disagreements.
|
|
28
54
|
|
|
29
55
|
## 1.7.3 - 2026-08-26
|
|
30
56
|
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
} = require("./lib/js-compiler");
|
|
12
12
|
const { normalizeDraft7, normalizeNullable, stripFormatAssertions } = require("./lib/draft7");
|
|
13
13
|
const { enabledKeywords, stripDisabledKeywords } = require("./lib/vocabularies");
|
|
14
|
+
const { needsNormalization } = require("./lib/schema-scan");
|
|
14
15
|
const { isV1Dialect } = require("./lib/dialect");
|
|
15
16
|
const { classify } = require("./lib/shape-classifier");
|
|
16
17
|
const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
|
|
@@ -372,12 +373,64 @@ function resolveSchemaByPath(rootSchema, schemaPath) {
|
|
|
372
373
|
// order AJV emits and what schema authors read top to bottom. Segments that
|
|
373
374
|
// cannot be resolved (cross-schema refs, normalized keys) end the walk; the
|
|
374
375
|
// stable sort then keeps such errors in engine emission order.
|
|
376
|
+
// The rank of a `schemaPath` under a given root is fixed: the schema does not
|
|
377
|
+
// change between validations, so neither does the answer. It was recomputed for
|
|
378
|
+
// every error of every failing document, and computing it is not cheap. Two
|
|
379
|
+
// caches, both keyed on things that do not change:
|
|
380
|
+
//
|
|
381
|
+
// rootSchema -> schemaPath -> rank, so a path is walked once ever
|
|
382
|
+
// node -> key -> its index, so the walk stops calling Object.keys and
|
|
383
|
+
// scanning the result for a string
|
|
384
|
+
//
|
|
385
|
+
// A failing route sees the same handful of schemaPaths over and over, which is
|
|
386
|
+
// what makes the first one worth having.
|
|
387
|
+
const _rankCache = new WeakMap();
|
|
388
|
+
const _keyIndexCache = new WeakMap();
|
|
389
|
+
|
|
390
|
+
function keyIndex(node, seg) {
|
|
391
|
+
let index = _keyIndexCache.get(node);
|
|
392
|
+
if (index === undefined) {
|
|
393
|
+
index = new Map();
|
|
394
|
+
const keys = Object.keys(node);
|
|
395
|
+
for (let i = 0; i < keys.length; i++) index.set(keys[i], i);
|
|
396
|
+
_keyIndexCache.set(node, index);
|
|
397
|
+
}
|
|
398
|
+
const at = index.get(seg);
|
|
399
|
+
return at === undefined ? -1 : at;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// `~1` and `~0` are the only escapes a JSON pointer has, and almost no schema
|
|
403
|
+
// key contains a tilde. Looking for one is far cheaper than two regex passes
|
|
404
|
+
// over every segment of every path.
|
|
405
|
+
function unescapePointerSegment(seg) {
|
|
406
|
+
return seg.indexOf('~') < 0 ? seg : seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
407
|
+
}
|
|
408
|
+
|
|
375
409
|
function schemaOrderRank(rootSchema, schemaPath) {
|
|
376
410
|
if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) return null;
|
|
377
|
-
|
|
411
|
+
if (rootSchema === null || typeof rootSchema !== 'object') return null;
|
|
412
|
+
|
|
413
|
+
let byPath = _rankCache.get(rootSchema);
|
|
414
|
+
if (byPath === undefined) { byPath = new Map(); _rankCache.set(rootSchema, byPath); }
|
|
415
|
+
const hit = byPath.get(schemaPath);
|
|
416
|
+
if (hit !== undefined) return hit;
|
|
417
|
+
|
|
418
|
+
const rank = _computeRank(rootSchema, schemaPath);
|
|
419
|
+
byPath.set(schemaPath, rank);
|
|
420
|
+
return rank;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function _computeRank(rootSchema, schemaPath) {
|
|
378
424
|
const rank = [];
|
|
379
425
|
let node = rootSchema;
|
|
380
|
-
|
|
426
|
+
let start = 1;
|
|
427
|
+
while (start <= schemaPath.length) {
|
|
428
|
+
let end = schemaPath.indexOf('/', start);
|
|
429
|
+
if (end < 0) end = schemaPath.length;
|
|
430
|
+
if (end === start) { start = end + 1; continue; } // what filter(Boolean) dropped
|
|
431
|
+
const seg = unescapePointerSegment(schemaPath.slice(start, end));
|
|
432
|
+
start = end + 1;
|
|
433
|
+
|
|
381
434
|
if (node == null || typeof node !== 'object') break;
|
|
382
435
|
if (Array.isArray(node)) {
|
|
383
436
|
const idx = Number(seg);
|
|
@@ -385,7 +438,7 @@ function schemaOrderRank(rootSchema, schemaPath) {
|
|
|
385
438
|
rank.push(idx);
|
|
386
439
|
node = node[idx];
|
|
387
440
|
} else {
|
|
388
|
-
const idx =
|
|
441
|
+
const idx = keyIndex(node, seg);
|
|
389
442
|
if (idx < 0) break;
|
|
390
443
|
rank.push(idx);
|
|
391
444
|
node = node[seg];
|
|
@@ -460,11 +513,20 @@ function _normalizeCallerSchema(s, inheritDraft7) {
|
|
|
460
513
|
const needsDraft7 = declares
|
|
461
514
|
? (s.$schema === 'http://json-schema.org/draft-07/schema#' || s.$schema === 'http://json-schema.org/draft-07/schema')
|
|
462
515
|
: !!inheritDraft7
|
|
516
|
+
// One walk answers whether there is anything to do. Almost always there is
|
|
517
|
+
// not, and then the serialize, clone, normalize, serialize, compare below is
|
|
518
|
+
// work spent to find that out. The walk over-reports rather than under, so a
|
|
519
|
+
// schema it clears is one no normalizer would have touched;
|
|
520
|
+
// `tests/test_schema_scan.js` holds that direction against the whole suite.
|
|
521
|
+
if (!needsNormalization(s, needsDraft7)) return s
|
|
522
|
+
|
|
463
523
|
const str = JSON.stringify(s)
|
|
464
524
|
const copy = _deepCloneWithSymbols(s)
|
|
465
525
|
if (needsDraft7) normalizeDraft7(copy, true)
|
|
466
526
|
normalizeNullable(copy)
|
|
467
527
|
// Return original when normalization produced no change, copy otherwise.
|
|
528
|
+
// Kept even though the walk has already said there is work, so that a walk
|
|
529
|
+
// which over-reports still returns exactly what it returned before.
|
|
468
530
|
// Change-detection uses JSON content only; symbols do not affect it.
|
|
469
531
|
return JSON.stringify(copy) === str ? s : copy
|
|
470
532
|
}
|
|
@@ -488,8 +550,33 @@ function declaredId(original, normalized) {
|
|
|
488
550
|
|
|
489
551
|
// `inheritDraft7` is true when the root schema is draft-07: a retrieved
|
|
490
552
|
// document that declares no dialect is read under the root's draft.
|
|
553
|
+
// The map is derived entirely from what the caller passed, so the same
|
|
554
|
+
// `schemas` gives the same map. A server building one validator per route over
|
|
555
|
+
// a shared registry rebuilt it once per route, normalizing and re-reading the
|
|
556
|
+
// `$id` of every registered schema each time. Keyed by the registry object,
|
|
557
|
+
// and by the draft it is read under, since that changes what normalization
|
|
558
|
+
// does to a document which declares no dialect of its own.
|
|
559
|
+
//
|
|
560
|
+
// Validators share the returned map, so anything that mutates one calls
|
|
561
|
+
// `_ownSchemaMap()` first. There are two such places: registering the vendored
|
|
562
|
+
// meta-schemas during compilation, and `addSchema()`.
|
|
563
|
+
const _schemaMapCache = new WeakMap()
|
|
564
|
+
|
|
491
565
|
function buildSchemaMap(schemas, inheritDraft7) {
|
|
492
566
|
if (!schemas) return null
|
|
567
|
+
const byDraft = _schemaMapCache.get(schemas)
|
|
568
|
+
if (byDraft) {
|
|
569
|
+
const hit = byDraft[inheritDraft7 ? 1 : 0]
|
|
570
|
+
if (hit) return hit
|
|
571
|
+
}
|
|
572
|
+
const map = _buildSchemaMap(schemas, inheritDraft7)
|
|
573
|
+
const slot = byDraft || [null, null]
|
|
574
|
+
slot[inheritDraft7 ? 1 : 0] = map
|
|
575
|
+
if (!byDraft) _schemaMapCache.set(schemas, slot)
|
|
576
|
+
return map
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function _buildSchemaMap(schemas, inheritDraft7) {
|
|
493
580
|
const map = new Map()
|
|
494
581
|
if (Array.isArray(schemas)) {
|
|
495
582
|
for (const s of schemas) {
|
|
@@ -648,7 +735,9 @@ class Validator {
|
|
|
648
735
|
// Built here rather than below because `$vocabulary` is resolved against
|
|
649
736
|
// it, and that resolution waits until compilation so a meta-schema
|
|
650
737
|
// registered by addSchema() still counts.
|
|
651
|
-
const
|
|
738
|
+
const shared = buildSchemaMap(options.schemas, rootIsDraft7);
|
|
739
|
+
const schemaMap = shared || new Map();
|
|
740
|
+
this._schemaMapShared = shared !== null;
|
|
652
741
|
this._schemaIsCallers = schemaObj === schema;
|
|
653
742
|
this._vocabulariesApplied = false;
|
|
654
743
|
|
|
@@ -824,6 +913,7 @@ class Validator {
|
|
|
824
913
|
// reference pay for the lookup.
|
|
825
914
|
if (this._schemaStr.includes('json-schema.org/draft')) {
|
|
826
915
|
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
916
|
+
this._ownSchemaMap();
|
|
827
917
|
for (const [id, meta] of METASCHEMAS) {
|
|
828
918
|
const bare = id.replace(/#$/, '');
|
|
829
919
|
for (const key of [id, bare, bare + '#', bare.replace(/^https:/, 'http:'), bare.replace(/^http:/, 'https:')]) {
|
|
@@ -1532,9 +1622,18 @@ class Validator {
|
|
|
1532
1622
|
const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
|
|
1533
1623
|
(root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
|
|
1534
1624
|
const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
|
|
1625
|
+
this._ownSchemaMap()
|
|
1535
1626
|
this._schemaMap.set(normalized.$id, normalized)
|
|
1536
1627
|
}
|
|
1537
1628
|
|
|
1629
|
+
// buildSchemaMap hands the same map to every validator built from the same
|
|
1630
|
+
// registry. Take a private copy before writing to it.
|
|
1631
|
+
_ownSchemaMap() {
|
|
1632
|
+
if (!this._schemaMapShared) return
|
|
1633
|
+
this._schemaMap = new Map(this._schemaMap)
|
|
1634
|
+
this._schemaMapShared = false
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1538
1637
|
_ensureCodegen() {
|
|
1539
1638
|
if (this._jsFn) return;
|
|
1540
1639
|
this._ensureVocabularies();
|
package/lib/js-compiler.js
CHANGED
|
@@ -1492,9 +1492,17 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1492
1492
|
function replaceTopLevel(code) {
|
|
1493
1493
|
let fnDepth = 0, result = '', i = 0
|
|
1494
1494
|
while (i < code.length) {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1495
|
+
// A nested closure keeps its own returns. Both spellings count: a
|
|
1496
|
+
// `function` keyword and an arrow with a block body. The inline pattern
|
|
1497
|
+
// compiler emits `(()=>{...return false...return true})()` and rewriting
|
|
1498
|
+
// those returns turned the arrow's boolean into an object, which made
|
|
1499
|
+
// `!(cond && obj)` false and let the hybrid accept what the boolean
|
|
1500
|
+
// program rejected.
|
|
1501
|
+
const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))
|
|
1502
|
+
const isArrowBlock = code.startsWith('=>{', i)
|
|
1503
|
+
if (isFunctionKw || isArrowBlock) {
|
|
1504
|
+
// Skip to the opening brace, then track all braces inside the body.
|
|
1505
|
+
let j = isArrowBlock ? i + 2 : i + 8
|
|
1498
1506
|
while (j < code.length && code[j] !== '{') j++
|
|
1499
1507
|
result += code.slice(i, j + 1)
|
|
1500
1508
|
i = j + 1
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One walk of a schema, answering in a single integer the questions that are
|
|
4
|
+
// currently answered by doing work and looking at the result.
|
|
5
|
+
//
|
|
6
|
+
// `_normalizeCallerSchema` decides whether a schema needs normalizing by
|
|
7
|
+
// serializing it, cloning it, normalizing the clone and serializing again to
|
|
8
|
+
// compare. Two serializations and a deep copy, on every schema, to find out
|
|
9
|
+
// that a modern schema needs nothing. The questions the normalizers actually
|
|
10
|
+
// ask are all of the form "does this key appear anywhere in the tree", and one
|
|
11
|
+
// traversal answers all of them at once. Daniel Lemire's rule from the C++
|
|
12
|
+
// side, a layer up: do not do the work to find out whether the work is needed.
|
|
13
|
+
//
|
|
14
|
+
// The walk deliberately descends through **every** object-valued key, not the
|
|
15
|
+
// list of subschema keywords the normalizers recurse through. That makes it a
|
|
16
|
+
// superset of what they visit, so it can report work where there is none, but
|
|
17
|
+
// never miss work there is. A false positive costs one clone. A false negative
|
|
18
|
+
// would hand an un-normalized schema to the engines, which is the silent
|
|
19
|
+
// acceptance this codebase treats as its worst failure. `tests/test_schema_scan.js`
|
|
20
|
+
// pins the direction of that error against the whole suite.
|
|
21
|
+
|
|
22
|
+
// One bit per question. Kept small and explicit; a schema which trips none of
|
|
23
|
+
// them scans to 0 and can skip normalization entirely.
|
|
24
|
+
const NULLABLE = 1 << 0; // `nullable`, which normalizeNullable folds into `type`
|
|
25
|
+
const REF_SIBLINGS = 1 << 1; // a draft-07 `$ref` carrying keywords which are ignored
|
|
26
|
+
const ANCHOR_ID = 1 << 2; // a fragment-only `$id`, which is an anchor in draft-07
|
|
27
|
+
const DEFINITIONS = 1 << 3; // `definitions`, renamed to `$defs`
|
|
28
|
+
const DEPENDENCIES = 1 << 4; // `dependencies`, split into two keywords
|
|
29
|
+
const TUPLE_ITEMS = 1 << 5; // array-valued `items`, split into `prefixItems`
|
|
30
|
+
|
|
31
|
+
// Everything only draft-07 normalization acts on. A schema of another dialect
|
|
32
|
+
// can carry these without them meaning anything, so they are read together
|
|
33
|
+
// with whether the document is draft-07 at all.
|
|
34
|
+
const DRAFT7_WORK = REF_SIBLINGS | ANCHOR_ID | DEFINITIONS | DEPENDENCIES | TUPLE_ITEMS;
|
|
35
|
+
|
|
36
|
+
// Copied from the normalizer rather than shared, so that a change there which
|
|
37
|
+
// forgets this file shows up as a differential test failure rather than as a
|
|
38
|
+
// silently wider scan. The test asserts the two agree.
|
|
39
|
+
const REF_SIBLINGS_KEPT = new Set([
|
|
40
|
+
'$ref', '$defs', 'definitions', '$schema', '$comment',
|
|
41
|
+
'title', 'description', 'examples', 'default', 'readOnly', 'writeOnly',
|
|
42
|
+
]);
|
|
43
|
+
const ANCHOR_ID_RE = /^#[A-Za-z][A-Za-z0-9_.:-]*$/;
|
|
44
|
+
|
|
45
|
+
// The answer depends only on the object, so it is remembered against it. A
|
|
46
|
+
// server building one validator per route over a shared registry hands the
|
|
47
|
+
// same registry objects to every one of them: fifty routes over twenty shared
|
|
48
|
+
// schemas scanned those twenty a thousand times. Identity caching is what
|
|
49
|
+
// `_identityCache` already does for whole compiled validators.
|
|
50
|
+
//
|
|
51
|
+
// This assumes a schema is not mutated after being handed to a Validator,
|
|
52
|
+
// which is already true of everything else here: the compile cache, the
|
|
53
|
+
// identity cache and the schema map would all be stale too.
|
|
54
|
+
const CACHE = new WeakMap();
|
|
55
|
+
|
|
56
|
+
function scan(schema) {
|
|
57
|
+
if (typeof schema !== 'object' || schema === null) return 0;
|
|
58
|
+
const hit = CACHE.get(schema);
|
|
59
|
+
if (hit !== undefined) return hit;
|
|
60
|
+
|
|
61
|
+
let bits = 0;
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
|
|
64
|
+
const walk = (node) => {
|
|
65
|
+
if (typeof node !== 'object' || node === null) return;
|
|
66
|
+
if (Array.isArray(node)) {
|
|
67
|
+
for (let i = 0; i < node.length; i++) walk(node[i]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (seen.has(node)) return;
|
|
71
|
+
seen.add(node);
|
|
72
|
+
|
|
73
|
+
if ('nullable' in node) bits |= NULLABLE;
|
|
74
|
+
if (node.definitions !== undefined && node.$defs === undefined) bits |= DEFINITIONS;
|
|
75
|
+
if (node.dependencies !== undefined) bits |= DEPENDENCIES;
|
|
76
|
+
if (Array.isArray(node.items)) bits |= TUPLE_ITEMS;
|
|
77
|
+
if (typeof node.$id === 'string' && ANCHOR_ID_RE.test(node.$id)) bits |= ANCHOR_ID;
|
|
78
|
+
|
|
79
|
+
// `Object.keys` rather than `for...in`: the prototype chain has nothing
|
|
80
|
+
// to contribute here, and walking it is the slower path in V8.
|
|
81
|
+
const keys = Object.keys(node);
|
|
82
|
+
if (typeof node.$ref === 'string') {
|
|
83
|
+
for (let i = 0; i < keys.length; i++) {
|
|
84
|
+
if (!REF_SIBLINGS_KEPT.has(keys[i])) { bits |= REF_SIBLINGS; break; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Every key, not just the subschema keywords, so this cannot miss a place
|
|
88
|
+
// the normalizers would reach.
|
|
89
|
+
for (let i = 0; i < keys.length; i++) walk(node[keys[i]]);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
walk(schema);
|
|
93
|
+
CACHE.set(schema, bits);
|
|
94
|
+
return bits;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Would normalization change this schema? `isDraft7` says whether the draft-07
|
|
98
|
+
// rules apply at all, which the caller already knows.
|
|
99
|
+
function needsNormalization(schema, isDraft7) {
|
|
100
|
+
const bits = scan(schema);
|
|
101
|
+
if (bits & NULLABLE) return true;
|
|
102
|
+
return isDraft7 ? (bits & DRAFT7_WORK) !== 0 : false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
scan,
|
|
107
|
+
needsNormalization,
|
|
108
|
+
NULLABLE,
|
|
109
|
+
REF_SIBLINGS,
|
|
110
|
+
ANCHOR_ID,
|
|
111
|
+
DEFINITIONS,
|
|
112
|
+
DEPENDENCIES,
|
|
113
|
+
TUPLE_ITEMS,
|
|
114
|
+
DRAFT7_WORK,
|
|
115
|
+
};
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
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_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",
|
|
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_hybrid_agreement.js && node tests/test_ajv_errors.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_schema_scan.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.8.
|
|
116
|
-
"@ata-validator/native-darwin-x64": "1.8.
|
|
117
|
-
"@ata-validator/native-linux-x64-gnu": "1.8.
|
|
118
|
-
"@ata-validator/native-linux-arm64-gnu": "1.8.
|
|
119
|
-
"@ata-validator/native-linux-x64-musl": "1.8.
|
|
120
|
-
"@ata-validator/native-linux-arm64-musl": "1.8.
|
|
121
|
-
"@ata-validator/native-win32-x64": "1.8.
|
|
115
|
+
"@ata-validator/native-darwin-arm64": "1.8.2",
|
|
116
|
+
"@ata-validator/native-darwin-x64": "1.8.2",
|
|
117
|
+
"@ata-validator/native-linux-x64-gnu": "1.8.2",
|
|
118
|
+
"@ata-validator/native-linux-arm64-gnu": "1.8.2",
|
|
119
|
+
"@ata-validator/native-linux-x64-musl": "1.8.2",
|
|
120
|
+
"@ata-validator/native-linux-arm64-musl": "1.8.2",
|
|
121
|
+
"@ata-validator/native-win32-x64": "1.8.2"
|
|
122
122
|
},
|
|
123
123
|
"peerDependencies": {
|
|
124
124
|
"yaml": "^2.0.0"
|