ata-validator 1.42.0 → 1.44.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 +10 -10
- package/index.js +56 -2
- package/lib/branch-collapse.js +18 -12
- package/lib/clone-emit.js +5 -2
- package/lib/data-position-cache.js +12 -6
- package/lib/draft7.js +5 -7
- package/lib/enrich-site.js +2 -2
- package/lib/js-compiler.js +788 -172
- package/lib/levenshtein.js +34 -22
- package/lib/native-positions.js +100 -0
- package/lib/rejections.js +14 -7
- package/lib/scan-compiler.js +210 -25
- package/lib/scan-runtime.js +13 -1
- package/lib/suggestions.js +27 -20
- package/lib/validator-core.js +94 -14
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -244,21 +244,21 @@ is the most common way to get a misleading number out of this library.
|
|
|
244
244
|
|
|
245
245
|
| | compiled with `ata build` | runtime `new Validator(schema)` |
|
|
246
246
|
|---|---|---|
|
|
247
|
-
| In a bundle, gzipped | **2.
|
|
248
|
-
| Time to a served request | **3.5 ms** |
|
|
247
|
+
| In a bundle, gzipped | **2.1 KB** | 110.7 KB |
|
|
248
|
+
| Time to a served request | **3.5 ms** | 8.2 ms |
|
|
249
249
|
| Schema known when | build time | any time |
|
|
250
250
|
|
|
251
251
|
The bundle row is the ten-field user schema in
|
|
252
252
|
`tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
|
|
253
|
-
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.
|
|
254
|
-
The startup row is a Hono route on Bun 1.4, median of
|
|
255
|
-
`benchmark/bundle`, against 3.
|
|
253
|
+
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.43.0.
|
|
254
|
+
The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
|
|
255
|
+
`benchmark/bundle`, against 3.4 ms for the same app doing no validation at all, so the
|
|
256
256
|
compiled path costs nothing measurable to start. The runtime figure is what it is because
|
|
257
257
|
a schema that arrives at run time can use any keyword, so the whole engine has to be
|
|
258
258
|
there. The compiled module imports nothing and contains only the checks your schema asks
|
|
259
259
|
for.
|
|
260
260
|
|
|
261
|
-
**On a server, use whichever fits your schemas.**
|
|
261
|
+
**On a server, use whichever fits your schemas.** 111 KB of JavaScript on a Node or Bun
|
|
262
262
|
process is not a cost anyone notices, and the runtime API is the simpler thing to reach
|
|
263
263
|
for. Speed is the same either way once warm.
|
|
264
264
|
|
|
@@ -491,7 +491,7 @@ const v = new Validator(schema, {
|
|
|
491
491
|
|
|
492
492
|
### Build-time compile (`ata compile`)
|
|
493
493
|
|
|
494
|
-
The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.
|
|
494
|
+
The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.1 KB gzipped, full error detail included, against 110.7 KB for the runtime bundled for the browser (ata 1.43.0).
|
|
495
495
|
|
|
496
496
|
```bash
|
|
497
497
|
npx ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
@@ -533,13 +533,13 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
|
|
|
533
533
|
Run with `--watch` during development for incremental rebuilds.
|
|
534
534
|
|
|
535
535
|
Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
|
|
536
|
-
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.
|
|
536
|
+
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.43.0:
|
|
537
537
|
|
|
538
538
|
| What the app imports | Size | Notes |
|
|
539
539
|
|---|---|---|
|
|
540
|
-
| `Validator` from `ata-validator` |
|
|
540
|
+
| `Validator` from `ata-validator` | 110.7 KB | The compiler ships with it, because a runtime schema can use any keyword |
|
|
541
541
|
| `isValid` from the compiled module | **1.3 KB** | Nothing else is reachable, so the error collector is dropped |
|
|
542
|
-
| `validate` from the compiled module | **
|
|
542
|
+
| `validate` from the compiled module | **1.9 KB** | Adds the detailed error collector |
|
|
543
543
|
|
|
544
544
|
`--abort-early` makes the generated source about three times smaller, and after
|
|
545
545
|
bundling it makes no difference: importing only `isValid` already leaves the error
|
package/index.js
CHANGED
|
@@ -434,6 +434,9 @@ function installCodegenPaths (ctx) {
|
|
|
434
434
|
}
|
|
435
435
|
return nativeText ? getNative().rawFastValidate(this._fastSlot, Buffer.from(jsonStr)) : undefined;
|
|
436
436
|
};
|
|
437
|
+
// The parsed document is left on `_lastParsed` for the rich layer around
|
|
438
|
+
// validateJSON, which hands it to the rejection so reading the errors does
|
|
439
|
+
// not parse the text again (see LazyJsonRejection).
|
|
437
440
|
const validateText = (jsonStr) => {
|
|
438
441
|
let obj;
|
|
439
442
|
try {
|
|
@@ -442,6 +445,7 @@ function installCodegenPaths (ctx) {
|
|
|
442
445
|
if (!(e instanceof SyntaxError)) throw e;
|
|
443
446
|
return _jsonSyntaxRejection(e);
|
|
444
447
|
}
|
|
448
|
+
this._lastParsed = obj;
|
|
445
449
|
return jsonValidateFn(obj);
|
|
446
450
|
};
|
|
447
451
|
this.validateJSON = useSimdjsonForLarge && !preprocess
|
|
@@ -503,7 +507,7 @@ function installCodegenPaths (ctx) {
|
|
|
503
507
|
// The verdict function alone, for isValidObject() before a full compile: the
|
|
504
508
|
// error and combined generators are left for the first rejection.
|
|
505
509
|
installCodegenPaths.compileVerdict = function compileVerdict () {
|
|
506
|
-
const { compileCacheKey, _compileCache, _bindVerdict, _rememberInstance } = core._internals;
|
|
510
|
+
const { compileCacheKey, _compileCache, _compileCacheSet, _bindVerdict, _rememberInstance } = core._internals;
|
|
507
511
|
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
508
512
|
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
509
513
|
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
@@ -532,7 +536,7 @@ installCodegenPaths.compileVerdict = function compileVerdict () {
|
|
|
532
536
|
// validator that later reuses this entry reports the same engine it
|
|
533
537
|
// would have compiled to.
|
|
534
538
|
if (!uf && !this._keywords) {
|
|
535
|
-
if (!cached)
|
|
539
|
+
if (!cached) _compileCacheSet(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
|
|
536
540
|
else cached.jsFn = jsFn;
|
|
537
541
|
}
|
|
538
542
|
}
|
|
@@ -878,8 +882,58 @@ function buildParse (self, decline, extended) {
|
|
|
878
882
|
// preprocess pass, the JSON-text scanner, the parse() copy and the
|
|
879
883
|
// ahead-of-time bundle methods. parse() and the bundle methods load on first
|
|
880
884
|
// use.
|
|
885
|
+
// Cold start. A validator's first COLD_CALLS calls are answered by an
|
|
886
|
+
// interpreted twin, built from the same schema and options with
|
|
887
|
+
// `engine: 'interpreter'`, which the test suite holds to the same answers and
|
|
888
|
+
// errors as the generated code. Generating code and having V8 compile it is
|
|
889
|
+
// the largest part of a first call on a large schema: from process start to
|
|
890
|
+
// one rejection with its error read, SchemaStore's SARIF schema took 87 ms
|
|
891
|
+
// with code generated at the first call and 31 ms with the twin answering.
|
|
892
|
+
// A validator still in use after that compiles as before, so a server reaches
|
|
893
|
+
// the generated code's speed within its first requests, and a command line
|
|
894
|
+
// tool or a short-lived function never pays to compile. Only where compiling
|
|
895
|
+
// costs enough to matter: a schema whose size with its references followed
|
|
896
|
+
// passes COLD_MIN characters (expandedChars in lib/js-compiler.js). A small
|
|
897
|
+
// schema compiles in about a millisecond and is generated at its first call
|
|
898
|
+
// as before, which also keeps every test that compares the generated code
|
|
899
|
+
// with the interpreter comparing the generated code. Not for a validator with
|
|
900
|
+
// an extension (_extendValidate, _extendChecks), which the twin would not
|
|
901
|
+
// carry. Returns the twin to answer this call, or null to compile.
|
|
902
|
+
const COLD_CALLS = 64;
|
|
903
|
+
const COLD_MIN = 16 * 1024;
|
|
904
|
+
// Whether the schema may expand past `limit`, answered from its text where
|
|
905
|
+
// that settles it. Without a `$ref` nothing expands, and expandedChars counts
|
|
906
|
+
// at most 8 for each character of the text, so a short schema is decided
|
|
907
|
+
// without walking it. The text is the one _ensureCompiled needs anyway. Walking
|
|
908
|
+
// every small route schema here cost a Fastify boot of ten routes 0.15 ms.
|
|
909
|
+
function mayExpandPast (self, limit) {
|
|
910
|
+
const str = self._schemaStr || (self._schemaStr = JSON.stringify(self._schemaObj));
|
|
911
|
+
if (str === undefined) return false;
|
|
912
|
+
if (str.length * 8 <= limit && !str.includes('"$ref"')) return false;
|
|
913
|
+
return jsCompiler.expandedChars(self._schemaObj, limit) > limit;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function coldTwin (self, Validator) {
|
|
917
|
+
const n = self._coldCalls === undefined ? 0 : self._coldCalls;
|
|
918
|
+
if (n >= COLD_CALLS || self._validateTail !== null || self._verdictTail !== null) return null;
|
|
919
|
+
let t = self._twin;
|
|
920
|
+
if (t === undefined) {
|
|
921
|
+
t = null;
|
|
922
|
+
if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
|
|
923
|
+
try { t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' })); } catch { t = null; }
|
|
924
|
+
}
|
|
925
|
+
self._twin = t;
|
|
926
|
+
}
|
|
927
|
+
if (t === null) return null;
|
|
928
|
+
self._coldCalls = n + 1;
|
|
929
|
+
return t;
|
|
930
|
+
}
|
|
931
|
+
|
|
881
932
|
core._registerCodegen({
|
|
882
933
|
jsCompiler,
|
|
934
|
+
coldTwin,
|
|
935
|
+
// The native locator for error frames, loaded with the first frame.
|
|
936
|
+
nativePositions: () => require('./lib/native-positions').nativeTargeted,
|
|
883
937
|
installPaths: installCodegenPaths,
|
|
884
938
|
compileVerdict: installCodegenPaths.compileVerdict,
|
|
885
939
|
installScanner: installCodegenPaths.installScanner,
|
package/lib/branch-collapse.js
CHANGED
|
@@ -44,6 +44,21 @@ function __ataScore (errs) {
|
|
|
44
44
|
// `title`, or the titles in `titles` when given (the generated code passes
|
|
45
45
|
// each branch's result as it is rather than copying it to add one).
|
|
46
46
|
// Returns the single error to report, or null when the keyword is satisfied.
|
|
47
|
+
// oneOf passed by more than one branch; generated code builds it from a count.
|
|
48
|
+
function __ataMulti (passIdx, total, pp, sp, o) {
|
|
49
|
+
const multi = {
|
|
50
|
+
code: 'ATA4002',
|
|
51
|
+
keyword: 'oneOf',
|
|
52
|
+
instancePath: pp || '',
|
|
53
|
+
path: pp || '',
|
|
54
|
+
schemaPath: sp,
|
|
55
|
+
message: 'value matched ' + passIdx.length + ' of ' + total + ' oneOf variants, expected exactly one',
|
|
56
|
+
params: { passingSchemas: passIdx },
|
|
57
|
+
};
|
|
58
|
+
if (o !== null) Object.defineProperty(multi, '_o', { value: o });
|
|
59
|
+
return multi;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
function __ataCollapse (kw, br, pp, sp, o, titles) {
|
|
48
63
|
let passN = 0;
|
|
49
64
|
for (let i = 0; i < br.length; i++) if (br[i].valid) passN++;
|
|
@@ -53,17 +68,7 @@ function __ataCollapse (kw, br, pp, sp, o, titles) {
|
|
|
53
68
|
if (pass.length > 1) {
|
|
54
69
|
const passIdx = [];
|
|
55
70
|
for (let i = 0; i < br.length; i++) if (br[i].valid) passIdx.push(i);
|
|
56
|
-
|
|
57
|
-
code: 'ATA4002',
|
|
58
|
-
keyword: 'oneOf',
|
|
59
|
-
instancePath: pp || '',
|
|
60
|
-
path: pp || '',
|
|
61
|
-
schemaPath: sp,
|
|
62
|
-
message: 'value matched ' + pass.length + ' of ' + br.length + ' oneOf variants, expected exactly one',
|
|
63
|
-
params: { passingSchemas: passIdx },
|
|
64
|
-
};
|
|
65
|
-
if (o !== null) Object.defineProperty(multi, '_o', { value: o });
|
|
66
|
-
return multi;
|
|
71
|
+
return __ataMulti(passIdx, br.length, pp, sp, o);
|
|
67
72
|
}
|
|
68
73
|
} else if (pass.length >= 1) {
|
|
69
74
|
return null;
|
|
@@ -97,12 +102,13 @@ function collapseBranches ({ keyword, branchResults, parentPath, parentSchemaPat
|
|
|
97
102
|
return __ataCollapse(keyword, branchResults, parentPath, parentSchemaPath, ordinal === undefined ? null : ordinal);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
// The
|
|
105
|
+
// The four declarations as source, for the generated code to carry. Derived
|
|
101
106
|
// from the functions above rather than written out again, so a fix to one
|
|
102
107
|
// cannot miss the other. `tests/test_branch_collapse.js` runs both.
|
|
103
108
|
function embedSource () {
|
|
104
109
|
return 'const __ATA_SEVERITY=' + JSON.stringify(__ATA_SEVERITY) + ';' +
|
|
105
110
|
'const __ataScore=' + __ataScore.toString() + ';' +
|
|
111
|
+
'const __ataMulti=' + __ataMulti.toString() + ';' +
|
|
106
112
|
'const __ataCollapse=' + __ataCollapse.toString() + ';';
|
|
107
113
|
}
|
|
108
114
|
|
package/lib/clone-emit.js
CHANGED
|
@@ -78,8 +78,11 @@ function inlineRefsForClone(root) {
|
|
|
78
78
|
for (const k of Object.keys(cur)) {
|
|
79
79
|
if (strip && k === 'default') continue;
|
|
80
80
|
const v = cur[k];
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// A property named "__proto__" is an own key of the copy: assigned, it
|
|
82
|
+
// set the copy's prototype and the property was gone from parse().
|
|
83
|
+
const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active, strip);
|
|
84
|
+
if (k === '__proto__') Object.defineProperty(out, k, { value: w, writable: true, enumerable: true, configurable: true });
|
|
85
|
+
else out[k] = w;
|
|
83
86
|
}
|
|
84
87
|
if (siblingDefault !== undefined) out.default = siblingDefault;
|
|
85
88
|
return out;
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const { buildDataPositionMap, buildTargetedPositionMap } = require('./data-positions');
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* Memoize the position map for the duration of a single validate() call.
|
|
7
8
|
* Caller passes the original buffer/string. Identity-keyed: same reference
|
|
8
9
|
* == same map. No global state, caller holds the cache instance.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
// `nativeTargeted`, when given, is a faster source of the same entries: the
|
|
13
|
+
// full entry point passes lib/native-positions.js, lite passes nothing.
|
|
14
|
+
function createCache (nativeTargeted) {
|
|
15
|
+
const _nativeTargeted = typeof nativeTargeted === 'function' ? nativeTargeted : null;
|
|
12
16
|
const wm = new WeakMap();
|
|
13
17
|
const sm = new Map(); // strings can't go in WeakMap; clear after each validate
|
|
14
18
|
let lastInput = null;
|
|
@@ -32,11 +36,13 @@ function createCache () {
|
|
|
32
36
|
? sm.get(input)
|
|
33
37
|
: (Buffer.isBuffer(input) ? wm.get(input) : undefined);
|
|
34
38
|
if (already) return already;
|
|
35
|
-
let filtered;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
let filtered = _nativeTargeted === null ? null : _nativeTargeted(input, wanted);
|
|
40
|
+
if (filtered === null) {
|
|
41
|
+
try {
|
|
42
|
+
filtered = buildTargetedPositionMap(input, wanted);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
let full;
|
|
42
48
|
const fallback = (key) => {
|
package/lib/draft7.js
CHANGED
|
@@ -49,13 +49,11 @@ function _normalize(schema) {
|
|
|
49
49
|
// dependencies → dependentSchemas + dependentRequired
|
|
50
50
|
if (schema.dependencies) {
|
|
51
51
|
for (const [key, value] of Object.entries(schema.dependencies)) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
schema.dependentSchemas[key] = value
|
|
58
|
-
}
|
|
52
|
+
// defineProperty: a dependency on a property named "__proto__" is an
|
|
53
|
+
// own key here, where assignment would set the map's prototype.
|
|
54
|
+
const target = Array.isArray(value) ? 'dependentRequired' : 'dependentSchemas'
|
|
55
|
+
if (!schema[target]) schema[target] = {}
|
|
56
|
+
Object.defineProperty(schema[target], key, { value, writable: true, enumerable: true, configurable: true })
|
|
59
57
|
}
|
|
60
58
|
delete schema.dependencies
|
|
61
59
|
}
|
package/lib/enrich-site.js
CHANGED
|
@@ -30,7 +30,7 @@ function errorSite (keyword, schemaPath, params, format) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function makeRich (site, path, params, message, value, root) {
|
|
33
|
+
function makeRich (site, path, params, message, value, root, spPrefix) {
|
|
34
34
|
// As resolveAt: a root that is null, undefined, '' or NaN resolves nothing.
|
|
35
35
|
const at = (!root && root !== 0 && root !== false) ? MISSING : value;
|
|
36
36
|
const out = {
|
|
@@ -40,7 +40,7 @@ function makeRich (site, path, params, message, value, root) {
|
|
|
40
40
|
path,
|
|
41
41
|
expected: site.expected === NO_EXPECTED ? expectedFor({ keyword: site.keyword, params }) : site.expected,
|
|
42
42
|
received: at === MISSING ? undefined : reprValue(at),
|
|
43
|
-
schemaPath: site.schemaPath,
|
|
43
|
+
schemaPath: spPrefix === undefined ? site.schemaPath : spPrefix + site.schemaPath,
|
|
44
44
|
docUrl: site.docUrl,
|
|
45
45
|
instancePath: path,
|
|
46
46
|
dataPath: path,
|