ata-validator 1.28.0 → 1.30.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 +3 -0
- package/build.d.ts +4 -2
- package/index.js +203 -75
- package/lib/aot-impl.js +28 -14
- package/lib/data-position-cache.js +46 -2
- package/lib/data-positions.js +200 -1
- package/lib/formats.js +114 -46
- package/lib/js-compiler.js +24 -7
- package/lib/ts-gen.js +5 -2
- package/lib/version.js +1 -1
- package/package.json +9 -10
- package/CHANGELOG.md +0 -750
package/README.md
CHANGED
|
@@ -703,6 +703,9 @@ npm run test:suite
|
|
|
703
703
|
|
|
704
704
|
## Project
|
|
705
705
|
|
|
706
|
+
- [CHANGELOG.md](CHANGELOG.md) records every release. It is kept in the
|
|
707
|
+
repository and not shipped in the npm package, where its history had grown to
|
|
708
|
+
about 13% of the install.
|
|
706
709
|
- [CONTRIBUTING.md](CONTRIBUTING.md) explains how to build the project and what a
|
|
707
710
|
pull request needs before it can be merged.
|
|
708
711
|
- [GOVERNANCE.md](GOVERNANCE.md) says who decides what, which changes the project
|
package/build.d.ts
CHANGED
|
@@ -144,8 +144,10 @@ export function bundleCompact(schemas: unknown[], options?: BundleStandaloneOpti
|
|
|
144
144
|
* Stable content hash of a schema, 16 hex characters, over a canonical JSON
|
|
145
145
|
* form (keys sorted at every level). Every module from
|
|
146
146
|
* {@link toStandaloneModule} exports its own `schemaHash`; comparing that
|
|
147
|
-
* against `schemaHash(currentSchema)` tells a build the
|
|
148
|
-
*
|
|
147
|
+
* against `schemaHash(currentSchema)` tells a build the schema has changed.
|
|
148
|
+
* It does not tell the build that ata has changed, since an upgrade leaves
|
|
149
|
+
* the hash matching, so compare the module's `ataVersion` against the
|
|
150
|
+
* installed version as well. An integrity aid, not a security boundary.
|
|
149
151
|
*/
|
|
150
152
|
export function schemaHash(schema: unknown): string;
|
|
151
153
|
|
package/index.js
CHANGED
|
@@ -462,11 +462,18 @@ const { rankFor: schemaOrderRank, ordinalFor: schemaOrdinal } = require('./lib/s
|
|
|
462
462
|
// that carry only message and path, such as the Standard Schema bridge.
|
|
463
463
|
// Prototype accessors, not per-instance ones: see LazyRejection.
|
|
464
464
|
class RichRejection {
|
|
465
|
-
|
|
465
|
+
// `rawInput` is the JSON text validateJSON was given, or null for validate(data).
|
|
466
|
+
// The position map it implies is built in the `errors` getter, not here: it is
|
|
467
|
+
// a full walk of the document, it is only ever read through an error's
|
|
468
|
+
// dataFrame, and building it on every rejection cost a caller that reads
|
|
469
|
+
// `.valid` about 460 microseconds on a 50 KB document. Holding the text rather
|
|
470
|
+
// than reading `self._lastRawInput` later also keeps the result independent of
|
|
471
|
+
// what the instance does after this call returns.
|
|
472
|
+
constructor(result, data, rawInput, self, root, enrich) {
|
|
466
473
|
this.valid = false;
|
|
467
474
|
this._result = result;
|
|
468
475
|
this._data = data;
|
|
469
|
-
this.
|
|
476
|
+
this._rawInput = rawInput;
|
|
470
477
|
this._self = self;
|
|
471
478
|
this._root = root;
|
|
472
479
|
this._enrich = enrich;
|
|
@@ -490,11 +497,17 @@ Object.defineProperty(RichRejection.prototype, 'errors', {
|
|
|
490
497
|
const enrich = this._enrich;
|
|
491
498
|
let raw = this._result.errors || [];
|
|
492
499
|
if (raw.length > 1) raw = sortErrorsBySchemaOrder(this._root, raw);
|
|
500
|
+
// The position map, resolved now that an error is actually being read.
|
|
501
|
+
let positions = null;
|
|
502
|
+
if (enrich && raw.length && this._rawInput != null) {
|
|
503
|
+
positions = self._pos().targeted(this._rawInput, wantedPointersFor(raw));
|
|
504
|
+
if (positions) self._posCache.reset();
|
|
505
|
+
}
|
|
493
506
|
// One options object for the whole list, not one per error.
|
|
494
507
|
const opts = enrich && raw.length
|
|
495
508
|
? {
|
|
496
509
|
data: this._data,
|
|
497
|
-
positions
|
|
510
|
+
positions,
|
|
498
511
|
schemaPositions: self._schemaPositions,
|
|
499
512
|
schemaFile: self._source ? self._source.path : undefined,
|
|
500
513
|
}
|
|
@@ -518,6 +531,123 @@ Object.defineProperty(RichRejection.prototype, 'errors', {
|
|
|
518
531
|
},
|
|
519
532
|
});
|
|
520
533
|
|
|
534
|
+
// The rejection validateJSON returns. Everything the text path adds over
|
|
535
|
+
// validate(data), the value tree enrichment needs, the position map, the
|
|
536
|
+
// diagnostic payload, happens on first access to `.errors`. Reading `.valid`
|
|
537
|
+
// touches none of it. The JSON text is held here rather than read back off the
|
|
538
|
+
// validator, so the result does not depend on what the instance does next.
|
|
539
|
+
class LazyJsonRejection {
|
|
540
|
+
constructor(result, jsonStr, self, enrich) {
|
|
541
|
+
this.valid = false;
|
|
542
|
+
this._result = result;
|
|
543
|
+
this._jsonStr = jsonStr;
|
|
544
|
+
this._self = self;
|
|
545
|
+
this._enrich = enrich;
|
|
546
|
+
this._cached = null;
|
|
547
|
+
}
|
|
548
|
+
toJSON() {
|
|
549
|
+
return { valid: false, errors: this.errors };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
553
|
+
enumerable: true,
|
|
554
|
+
configurable: true,
|
|
555
|
+
get() {
|
|
556
|
+
if (this._cached !== null) return this._cached;
|
|
557
|
+
const self = this._self;
|
|
558
|
+
const enrich = this._enrich;
|
|
559
|
+
const jsonStr = this._jsonStr;
|
|
560
|
+
// Reading the inner errors realizes the inner lazy layer, if there was one.
|
|
561
|
+
const raw = this._result.errors || [];
|
|
562
|
+
if (!raw.length) { this._cached = raw; return raw; }
|
|
563
|
+
|
|
564
|
+
// The enrich pass plucks `received` from the value tree and the suggestion
|
|
565
|
+
// engine (required-typo, format hints, coercion nudges) walks it too.
|
|
566
|
+
let parsedData;
|
|
567
|
+
try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
|
|
568
|
+
|
|
569
|
+
// Errors the inner path already enriched carry a docUrl: only enrich() sets
|
|
570
|
+
// one. `code` is not a safe signal, because branch-collapse attaches codes
|
|
571
|
+
// to raw errors, and detecting on it left every collapsed oneOf/anyOf error
|
|
572
|
+
// unenriched on the text path.
|
|
573
|
+
if (!raw[0] || !raw[0].docUrl) {
|
|
574
|
+
const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
|
|
575
|
+
// Declaration order, as validate() applies it; the text path used to
|
|
576
|
+
// enrich in emission order.
|
|
577
|
+
const ordered = raw.length > 1 ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
|
|
578
|
+
const enrichOpts = {
|
|
579
|
+
data: parsedData,
|
|
580
|
+
positions,
|
|
581
|
+
schemaPositions: self._schemaPositions,
|
|
582
|
+
schemaFile: self._source ? self._source.path : undefined,
|
|
583
|
+
};
|
|
584
|
+
const enriched = ordered.map((e) => enrich(e, enrichOpts));
|
|
585
|
+
if (enriched.length > 1) attachRelated(enriched);
|
|
586
|
+
attachDiagnosticSource(enriched, {
|
|
587
|
+
data: parsedData,
|
|
588
|
+
text: jsonStr,
|
|
589
|
+
positions,
|
|
590
|
+
schema: self._schemaObj,
|
|
591
|
+
mutatesInput: self._mutatesInput === true,
|
|
592
|
+
});
|
|
593
|
+
if (positions) self._posCache.reset();
|
|
594
|
+
this._cached = enriched;
|
|
595
|
+
return enriched;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Already enriched, so the frames are usually attached too. Only a gap in
|
|
599
|
+
// them is worth another walk of the document: resolving the map to discover
|
|
600
|
+
// there was nothing to fill cost a second full walk on every rejection.
|
|
601
|
+
if (raw.some((e) => e && !e.dataFrame)) {
|
|
602
|
+
const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw.filter((e) => e && !e.dataFrame)));
|
|
603
|
+
if (positions) {
|
|
604
|
+
for (const e of raw) {
|
|
605
|
+
if (e && !e.dataFrame) {
|
|
606
|
+
const path = e.path != null ? e.path : (e.instancePath || '');
|
|
607
|
+
const p = positions[path];
|
|
608
|
+
if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
self._posCache.reset();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (raw.length > 1) attachRelated(raw);
|
|
615
|
+
attachDiagnosticSource(raw, {
|
|
616
|
+
data: parsedData,
|
|
617
|
+
text: jsonStr,
|
|
618
|
+
schema: self._schemaObj,
|
|
619
|
+
mutatesInput: self._mutatesInput === true,
|
|
620
|
+
});
|
|
621
|
+
this._cached = raw;
|
|
622
|
+
return raw;
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// The pointers a set of errors will ask the position map about. lib/enrich-error
|
|
627
|
+
// looks up the error's own path, and for an additional or unevaluated property
|
|
628
|
+
// the child pointer named in `params`, unescaped, which is the form it asks for.
|
|
629
|
+
// The escaped form goes in too: including a pointer that is never read costs
|
|
630
|
+
// nothing, and the cache answers anything outside this set from the full map
|
|
631
|
+
// rather than reporting no position.
|
|
632
|
+
function wantedPointersFor (errors) {
|
|
633
|
+
const wanted = new Set();
|
|
634
|
+
for (const e of errors) {
|
|
635
|
+
if (!e) continue;
|
|
636
|
+
const path = e.path != null ? e.path : (e.instancePath || '');
|
|
637
|
+
wanted.add(path);
|
|
638
|
+
if (e.instancePath != null && e.instancePath !== path) wanted.add(e.instancePath);
|
|
639
|
+
const params = e.params;
|
|
640
|
+
const named = params && (params.additionalProperty || params.unevaluatedProperty);
|
|
641
|
+
if (typeof named === 'string') {
|
|
642
|
+
wanted.add(path + '/' + named);
|
|
643
|
+
if (named.indexOf('~') !== -1 || named.indexOf('/') !== -1) {
|
|
644
|
+
wanted.add(path + '/' + named.replace(/~/g, '~0').replace(/\//g, '~1'));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return wanted;
|
|
649
|
+
}
|
|
650
|
+
|
|
521
651
|
// A raw error without the `_o` ordering key, for the legacy error shape.
|
|
522
652
|
function stripOrdinal(e) {
|
|
523
653
|
if (e === null || typeof e !== 'object' || e._o === undefined) return e;
|
|
@@ -1345,10 +1475,32 @@ class Validator {
|
|
|
1345
1475
|
// The factory bakes the error function in as an argument and never
|
|
1346
1476
|
// calls it for a document that passes, so a resolver here costs the
|
|
1347
1477
|
// accepted path nothing and keeps the compile off the first call.
|
|
1348
|
-
|
|
1478
|
+
// Until the first rejection this is the hybrid: the verdict function,
|
|
1479
|
+
// with the error resolver baked in and never called for a document that
|
|
1480
|
+
// passes. That keeps the combined function's compile off the first call,
|
|
1481
|
+
// which is two thirds of what a first call costs.
|
|
1482
|
+
//
|
|
1483
|
+
// From the first rejection on, the combined function answers directly.
|
|
1484
|
+
// It decides and collects in one pass, so the verdict pass in front of it
|
|
1485
|
+
// was validating the document a second time: 134.1 microseconds against
|
|
1486
|
+
// its 45.3 on a 1000-user array. Swapping rather than starting there keeps
|
|
1487
|
+
// the lazy compile, and swapping at all is only free because the combined
|
|
1488
|
+
// function now costs what the verdict function costs on accepted
|
|
1489
|
+
// documents (1.02x on that array, 0.99x on a small body) since
|
|
1490
|
+
// additionalProperties stopped materialising its key array. The
|
|
1491
|
+
// indirection this needs measured inside the noise at both sizes.
|
|
1492
|
+
let impl = null;
|
|
1493
|
+
const onReject = (data) => {
|
|
1494
|
+
const combined = combinedIfSafe();
|
|
1495
|
+
if (combined) { impl = combined; return combined(data); }
|
|
1496
|
+
return errOnly(data);
|
|
1497
|
+
};
|
|
1498
|
+
const hybridFn = jsFn._hybridFactory(VALID_RESULT, onReject);
|
|
1499
|
+
impl = hybridFn;
|
|
1500
|
+
const run = (data) => impl(data);
|
|
1349
1501
|
this.validate = preprocess
|
|
1350
|
-
? (data) => { preprocess(data); return
|
|
1351
|
-
:
|
|
1502
|
+
? (data) => { preprocess(data); return run(data); }
|
|
1503
|
+
: run;
|
|
1352
1504
|
} else {
|
|
1353
1505
|
// No hybrid factory, so the assembly needs the function itself rather
|
|
1354
1506
|
// than a reference it can call later: build it now.
|
|
@@ -1425,7 +1577,11 @@ class Validator {
|
|
|
1425
1577
|
: jsonValidateInner;
|
|
1426
1578
|
this.validateJSON = useSimdjsonForLarge && native && !preprocess
|
|
1427
1579
|
? (jsonStr) => {
|
|
1428
|
-
|
|
1580
|
+
// `_skipNativeFast` is set by the scanner short-circuit below when it
|
|
1581
|
+
// has already decided the document is invalid. The encode and the
|
|
1582
|
+
// native call would run only to return false, and the error path
|
|
1583
|
+
// underneath does not need them.
|
|
1584
|
+
if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
|
|
1429
1585
|
this._ensureNative();
|
|
1430
1586
|
const buf = Buffer.from(jsonStr);
|
|
1431
1587
|
if (native.rawFastValidate(this._fastSlot, buf))
|
|
@@ -1681,15 +1837,15 @@ class Validator {
|
|
|
1681
1837
|
// abortEarly returns the shared ATA9000 stub; preserve it as-is so the
|
|
1682
1838
|
// perf fast path stays allocation-free and the documented code stays stable.
|
|
1683
1839
|
if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
|
|
1687
|
-
|
|
1840
|
+
// The raw input travels with the rejection when validateJSON set one.
|
|
1841
|
+
// The map it implies is built on first access to `.errors`, so a
|
|
1842
|
+
// caller reading only `.valid` does not pay for a document walk.
|
|
1843
|
+
const rawInput = enrich ? self._lastRawInput : null;
|
|
1688
1844
|
// One instance of a class with prototype accessors. An object
|
|
1689
1845
|
// literal with a getter here cost a closure plus an accessor
|
|
1690
1846
|
// definition on every rejection, several hundred nanoseconds
|
|
1691
1847
|
// before any error was read.
|
|
1692
|
-
return new RichRejection(result, data,
|
|
1848
|
+
return new RichRejection(result, data, rawInput, self, root, enrich);
|
|
1693
1849
|
}
|
|
1694
1850
|
return result;
|
|
1695
1851
|
};
|
|
@@ -1700,74 +1856,23 @@ class Validator {
|
|
|
1700
1856
|
if (this._richErrors && this.validateJSON) {
|
|
1701
1857
|
const innerJson = this.validateJSON;
|
|
1702
1858
|
this.validateJSON = (jsonStr) => {
|
|
1859
|
+
// The inner path reads _lastRawInput to hand the raw text to the
|
|
1860
|
+
// rejection it builds. Cleared as soon as it returns: the rejection
|
|
1861
|
+
// carries the text itself, so nothing outlives the call.
|
|
1703
1862
|
this._lastRawInput = jsonStr;
|
|
1704
1863
|
let result;
|
|
1705
1864
|
try {
|
|
1706
1865
|
result = innerJson(jsonStr);
|
|
1707
1866
|
} finally {
|
|
1708
|
-
|
|
1709
|
-
// clear after enrich, or in the early-return path.
|
|
1867
|
+
this._lastRawInput = null;
|
|
1710
1868
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
// it left every collapsed oneOf/anyOf error unenriched on the
|
|
1718
|
-
// text path.
|
|
1719
|
-
const first = result.errors[0];
|
|
1720
|
-
// Re-parse the input once so the enrich pass can pluck `received`
|
|
1721
|
-
// and feed the suggestion engine (required-typo, format hints,
|
|
1722
|
-
// coercion nudges all need the live value tree), and so the
|
|
1723
|
-
// diagnostic payload carries the data on both paths below.
|
|
1724
|
-
let parsedData;
|
|
1725
|
-
try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
|
|
1726
|
-
if (!first || !first.docUrl) {
|
|
1727
|
-
const positions = (this._lastRawInput != null) ? this._pos().get(this._lastRawInput) : null;
|
|
1728
|
-
// Declaration order, as validate() applies it; the text path
|
|
1729
|
-
// used to enrich in emission order.
|
|
1730
|
-
const ordered = result.errors.length > 1 ? sortErrorsBySchemaOrder(this._schemaObj, result.errors) : result.errors;
|
|
1731
|
-
const enrichOpts = {
|
|
1732
|
-
data: parsedData,
|
|
1733
|
-
positions,
|
|
1734
|
-
schemaPositions: this._schemaPositions,
|
|
1735
|
-
schemaFile: this._source ? this._source.path : undefined,
|
|
1736
|
-
};
|
|
1737
|
-
const enriched = ordered.map((e) => enrich(e, enrichOpts));
|
|
1738
|
-
if (enriched.length > 1) attachRelated(enriched);
|
|
1739
|
-
attachDiagnosticSource(enriched, {
|
|
1740
|
-
data: parsedData,
|
|
1741
|
-
text: jsonStr,
|
|
1742
|
-
positions,
|
|
1743
|
-
schema: this._schemaObj,
|
|
1744
|
-
mutatesInput: this._mutatesInput === true,
|
|
1745
|
-
});
|
|
1746
|
-
if (positions) this._posCache.reset();
|
|
1747
|
-
this._lastRawInput = null;
|
|
1748
|
-
return { valid: false, errors: enriched };
|
|
1749
|
-
}
|
|
1750
|
-
// Already-enriched path: still attach dataFrame if missing.
|
|
1751
|
-
const positions = (this._lastRawInput != null) ? this._pos().get(this._lastRawInput) : null;
|
|
1752
|
-
if (positions) {
|
|
1753
|
-
for (const e of result.errors) {
|
|
1754
|
-
if (e && !e.dataFrame) {
|
|
1755
|
-
const path = e.path != null ? e.path : (e.instancePath || '');
|
|
1756
|
-
const p = positions[path];
|
|
1757
|
-
if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
this._posCache.reset();
|
|
1761
|
-
}
|
|
1762
|
-
if (result.errors.length > 1) attachRelated(result.errors);
|
|
1763
|
-
attachDiagnosticSource(result.errors, {
|
|
1764
|
-
data: parsedData,
|
|
1765
|
-
text: jsonStr,
|
|
1766
|
-
schema: this._schemaObj,
|
|
1767
|
-
mutatesInput: this._mutatesInput === true,
|
|
1768
|
-
});
|
|
1869
|
+
// Every diagnostic the text path adds is deferred. Deciding here
|
|
1870
|
+
// whether there is anything to add would mean reading `result.errors`,
|
|
1871
|
+
// and on the codegen path that realizes the inner lazy layer, which is
|
|
1872
|
+
// the document walk this exists to avoid.
|
|
1873
|
+
if (result && result.valid === false) {
|
|
1874
|
+
return new LazyJsonRejection(result, jsonStr, this, enrich);
|
|
1769
1875
|
}
|
|
1770
|
-
this._lastRawInput = null;
|
|
1771
1876
|
return result;
|
|
1772
1877
|
};
|
|
1773
1878
|
}
|
|
@@ -1925,8 +2030,23 @@ class Validator {
|
|
|
1925
2030
|
};
|
|
1926
2031
|
return self.isValidJSON(jsonStr);
|
|
1927
2032
|
};
|
|
1928
|
-
|
|
2033
|
+
// validateJSON gets the same short-circuit isValidJSON has. The verdict is
|
|
2034
|
+
// a property of the text, and the scanner reads the text once and
|
|
2035
|
+
// allocates nothing; above the simdjson threshold the path underneath
|
|
2036
|
+
// encoded the whole document to a Buffer and called the native validator
|
|
2037
|
+
// instead, which measured 340 microseconds against the scanner's 191 on a
|
|
2038
|
+
// 149 KB config, the same against the published addon as against a local
|
|
2039
|
+
// build.
|
|
2040
|
+
//
|
|
2041
|
+
// An accepted document stops at the scanner. A rejected one still has to
|
|
2042
|
+
// produce errors, so it goes on to the path below, which is the
|
|
2043
|
+
// rich-errors wrapper and everything under it; the flag only tells that
|
|
2044
|
+
// path to skip an encode and a native call that would return false. Doing
|
|
2045
|
+
// it the other way, returning errors from the inner function directly,
|
|
2046
|
+
// would hand back errors that never passed through enrichment.
|
|
2047
|
+
{
|
|
1929
2048
|
const validateByParsing = this.validateJSON;
|
|
2049
|
+
const abortEarly = !!options.abortEarly;
|
|
1930
2050
|
this.validateJSON = (jsonStr) => {
|
|
1931
2051
|
const scan = self._ensureScanner();
|
|
1932
2052
|
if (scan === undefined) return validateByParsing(jsonStr);
|
|
@@ -1935,7 +2055,15 @@ class Validator {
|
|
|
1935
2055
|
if (typeof text === 'string') {
|
|
1936
2056
|
const r = scan(text);
|
|
1937
2057
|
if (r === 1) return VALID_RESULT;
|
|
1938
|
-
if (r === 0)
|
|
2058
|
+
if (r === 0) {
|
|
2059
|
+
if (abortEarly) return ABORT_EARLY_RESULT;
|
|
2060
|
+
self._skipNativeFast = true;
|
|
2061
|
+
try {
|
|
2062
|
+
return validateByParsing(text);
|
|
2063
|
+
} finally {
|
|
2064
|
+
self._skipNativeFast = false;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1939
2067
|
}
|
|
1940
2068
|
return validateByParsing(text);
|
|
1941
2069
|
};
|
package/lib/aot-impl.js
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// loadBundle(Validator, mods, schemas, opts)
|
|
15
15
|
|
|
16
16
|
const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions } = require('./js-compiler');
|
|
17
|
-
const {
|
|
17
|
+
const { buildTargetedPositionMap } = require('./data-positions');
|
|
18
18
|
const { schemaHash } = require('./schema-hash');
|
|
19
|
+
const ATA_VERSION = require('./version');
|
|
19
20
|
const SAFE_REGEX_SOURCE = require('./safe-regex-source');
|
|
20
21
|
|
|
21
22
|
// Embedded verbatim in standalone modules so the output file has no runtime
|
|
@@ -119,7 +120,7 @@ function toStandalone(validator) {
|
|
|
119
120
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
120
121
|
|
|
121
122
|
const closureSrc = closureDeclLines(jsFn).join('\n');
|
|
122
|
-
return `// Auto-generated by ata-validator
|
|
123
|
+
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit
|
|
123
124
|
'use strict';
|
|
124
125
|
${_CP_LEN_SOURCE}
|
|
125
126
|
${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
|
|
@@ -594,10 +595,15 @@ function toStandaloneModule(validator, opts) {
|
|
|
594
595
|
// dataFrame (byte offset, line, col, source line) to every error by
|
|
595
596
|
// walking the original text once. Off by default for the same reason as
|
|
596
597
|
// parse(): it costs bytes in every emitted module. Ask for it with
|
|
597
|
-
// { positions: true }. The walker is the runtime's
|
|
598
|
+
// { positions: true }. The walker is the runtime's buildTargetedPositionMap,
|
|
598
599
|
// embedded verbatim via toString so the two cannot drift; that is why the
|
|
599
|
-
// function in lib/data-positions.js must stay self-contained.
|
|
600
|
-
|
|
600
|
+
// function in lib/data-positions.js must stay self-contained. It is given the
|
|
601
|
+
// pointers the errors name, which is the only thing this module looks up, so a
|
|
602
|
+
// subtree that cannot hold one is scanned to its end and never walked: framing
|
|
603
|
+
// an error went from 8.02x a JSON.parse of the document to 1.72x, and 3.11x in
|
|
604
|
+
// the worst case of a pointer at the very end. It is also 125 characters
|
|
605
|
+
// smaller than the full-map builder it replaces.
|
|
606
|
+
const positionsCore = !(opts && opts.positions) ? '' : `const _ataPosMap = ${buildTargetedPositionMap.toString()};
|
|
601
607
|
function _ataFrame(p) { return { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text }; }
|
|
602
608
|
function validateJSON(text) {
|
|
603
609
|
const s = String(text);
|
|
@@ -605,13 +611,15 @@ function validateJSON(text) {
|
|
|
605
611
|
try { data = JSON.parse(s); } catch (e) {
|
|
606
612
|
const nl = s.indexOf(String.fromCharCode(10));
|
|
607
613
|
let frame = { byteOffset: 0, length: s.length, line: 1, col: 1, text: nl === -1 ? s : s.slice(0, nl) };
|
|
608
|
-
try { const m = _ataPosMap(s); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
|
|
614
|
+
try { const m = _ataPosMap(s, new Set([''])); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
|
|
609
615
|
return { valid: false, errors: [{ code: 'ATA9001', message: 'invalid JSON document', keyword: '__parse__', path: '', instancePath: '', dataFrame: frame }] };
|
|
610
616
|
}
|
|
611
617
|
const r = validate(data);
|
|
612
618
|
if (r.valid || !r.errors || !r.errors.length) return r;
|
|
613
619
|
let map;
|
|
614
|
-
|
|
620
|
+
const _want = new Set();
|
|
621
|
+
for (const e of r.errors) _want.add(e.instancePath != null ? e.instancePath : (e.path || ''));
|
|
622
|
+
try { map = _ataPosMap(s, _want); } catch (_) { return r; }
|
|
615
623
|
const errors = r.errors.map((e) => {
|
|
616
624
|
const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
|
|
617
625
|
const p = map[ptr];
|
|
@@ -627,10 +635,15 @@ function validateJSON(text) {
|
|
|
627
635
|
// can compare it against schemaHash(currentSchema) and know the module is
|
|
628
636
|
// stale without embedding its own fingerprint.
|
|
629
637
|
const hashSrc = schemaHash(validator._rawSchema !== undefined ? validator._rawSchema : validator._schemaObj);
|
|
630
|
-
|
|
638
|
+
// The schema hash answers "is this module built from a different schema" and
|
|
639
|
+
// nothing else. Upgrading ata and not re-running the generate step leaves the
|
|
640
|
+
// hash matching, so the stale module reads as current. The module cannot ask
|
|
641
|
+
// the installed ata itself, since it imports nothing, so it carries the
|
|
642
|
+
// version that wrote it and a build compares that too.
|
|
643
|
+
const hashDecl = `const schemaHash = ${JSON.stringify(hashSrc)};\nconst ataVersion = ${JSON.stringify(ATA_VERSION)};\n`;
|
|
631
644
|
|
|
632
645
|
let names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
|
|
633
|
-
names += ', schemaHash';
|
|
646
|
+
names += ', schemaHash, ataVersion';
|
|
634
647
|
if (positionsCore) names += ', validateJSON';
|
|
635
648
|
const parseAlias = cloneExpr ? ', _ataParse as parse' : '';
|
|
636
649
|
const parseProp = cloneExpr ? ', parse: _ataParse' : '';
|
|
@@ -644,8 +657,9 @@ function validateJSON(text) {
|
|
|
644
657
|
if (opts && opts.parse && !cloneExpr) {
|
|
645
658
|
degradedNote += '// NOTE: parse() was requested but could not be generated for this schema;\n// the module has no parse export. Validate and strip with the runtime Validator.\n';
|
|
646
659
|
}
|
|
647
|
-
return `// Auto-generated by ata-validator
|
|
660
|
+
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
|
|
648
661
|
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
662
|
+
// Re-run the build after upgrading ata: the version above is what wrote this.
|
|
649
663
|
${degradedNote}'use strict';
|
|
650
664
|
${_CP_LEN_SOURCE}
|
|
651
665
|
${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
@@ -786,10 +800,10 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
786
800
|
: '';
|
|
787
801
|
if (format === 'esm') {
|
|
788
802
|
const extra = bundleInjects ? 'export { validators, setFormats };' : 'export { validators };';
|
|
789
|
-
return `// Auto-generated by ata-validator
|
|
803
|
+
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit\n${safeEmbed}${registry}const R=${R};\nconst validators=${arr};\nexport default validators;\n${extra}\n`;
|
|
790
804
|
}
|
|
791
805
|
const attach = bundleInjects ? 'module.exports.setFormats=setFormats;\n' : '';
|
|
792
|
-
return
|
|
806
|
+
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit\n'use strict';\n${safeEmbed}${registry}var R=${R};\nmodule.exports=[${fns.join(',')}];\n${attach}`;
|
|
793
807
|
}
|
|
794
808
|
|
|
795
809
|
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
@@ -898,8 +912,8 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
898
912
|
// Generate compact bundle
|
|
899
913
|
const isEsm = format === 'esm';
|
|
900
914
|
let out = isEsm
|
|
901
|
-
?
|
|
902
|
-
:
|
|
915
|
+
? `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit\n`
|
|
916
|
+
: `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit\n'use strict';\n`;
|
|
903
917
|
if (bundleUsesSafeRe) out += getSafeRegexEmbed() + '\n';
|
|
904
918
|
if (sharedDecls.length) out += sharedDecls.join('\n') + '\n';
|
|
905
919
|
const declKW = isEsm ? 'const' : 'var';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { buildDataPositionMap } = require('./data-positions');
|
|
3
|
+
const { buildDataPositionMap, buildTargetedPositionMap } = require('./data-positions');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Memoize the position map for the duration of a single validate() call.
|
|
@@ -12,7 +12,50 @@ function createCache () {
|
|
|
12
12
|
const wm = new WeakMap();
|
|
13
13
|
const sm = new Map(); // strings can't go in WeakMap; clear after each validate
|
|
14
14
|
let lastInput = null;
|
|
15
|
-
|
|
15
|
+
const cache = {
|
|
16
|
+
/**
|
|
17
|
+
* The entries for a known set of pointers, which is what a rejection needs:
|
|
18
|
+
* recording one per node in the document was the dominant cost of reading an
|
|
19
|
+
* error's frame.
|
|
20
|
+
*
|
|
21
|
+
* The caller derives `wanted` from the errors, and a derivation that misses a
|
|
22
|
+
* pointer must not cost a frame, so the result answers any pointer outside
|
|
23
|
+
* the set by building the full map once and reading from that. A wrong
|
|
24
|
+
* derivation is then slow rather than silently short of frames, which is the
|
|
25
|
+
* only acceptable direction for this trade: a missing caret is the failure
|
|
26
|
+
* nobody reports.
|
|
27
|
+
*/
|
|
28
|
+
targeted (input, wanted) {
|
|
29
|
+
if (input == null) return null;
|
|
30
|
+
if (!wanted || wanted.size === 0) return null;
|
|
31
|
+
const already = typeof input === 'string'
|
|
32
|
+
? sm.get(input)
|
|
33
|
+
: (Buffer.isBuffer(input) ? wm.get(input) : undefined);
|
|
34
|
+
if (already) return already;
|
|
35
|
+
let filtered;
|
|
36
|
+
try {
|
|
37
|
+
filtered = buildTargetedPositionMap(input, wanted);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
let full;
|
|
42
|
+
const fallback = (key) => {
|
|
43
|
+
if (wanted.has(key)) return undefined; // asked for, absent from the document
|
|
44
|
+
if (full === undefined) full = cache.get(input) || null;
|
|
45
|
+
return full ? full[key] : undefined;
|
|
46
|
+
};
|
|
47
|
+
return new Proxy(filtered, {
|
|
48
|
+
get (target, key) {
|
|
49
|
+
if (typeof key !== 'string') return target[key];
|
|
50
|
+
if (key in target) return target[key];
|
|
51
|
+
return fallback(key);
|
|
52
|
+
},
|
|
53
|
+
has (target, key) {
|
|
54
|
+
if (typeof key !== 'string') return key in target;
|
|
55
|
+
return (key in target) || fallback(key) !== undefined;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
16
59
|
get (input) {
|
|
17
60
|
if (input == null) return null;
|
|
18
61
|
if (typeof input === 'string') {
|
|
@@ -38,6 +81,7 @@ function createCache () {
|
|
|
38
81
|
lastInput = null;
|
|
39
82
|
},
|
|
40
83
|
};
|
|
84
|
+
return cache;
|
|
41
85
|
}
|
|
42
86
|
|
|
43
87
|
module.exports = { createCache };
|