ata-validator 1.22.1 → 1.24.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 +30 -0
- package/bin/ata.js +27 -1
- package/compat.d.ts +5 -0
- package/compat.js +11 -2
- package/index.js +256 -70
- package/lib/aot-build.js +24 -2
- package/lib/aot-impl.js +14 -1
- package/lib/compat-errors.js +49 -22
- package/lib/js-compiler.js +15 -4
- package/lib/scan-compiler.js +1007 -0
- package/lib/scan-runtime.js +158 -0
- package/lib/strict-check.js +195 -0
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
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.24.0 - 2026-09-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- A schema-directed scanner: `isValidJSON()` answers the verdict from the JSON text without building the document. Parsing is about three quarters of the cost of a request that only needs yes or no, and a rejection now stops at the byte that caused it. The scanner compiles per schema, on the codegen path only, after 64 calls (generating one costs about 20 µs, so a caller that checks one document never pays), and it declines anything outside its supported core: `type`, `properties`, `required`, `additionalProperties`, `items`, `prefixItems`, the length, size and range keywords, `pattern`, `format`, `const`, `enum`, local `$ref` (inlined, acyclic, no base-changing keywords below the root), `allOf` (merged, with the `additionalProperties` cross-branch rule written out in full), and `unevaluated*` where it is provably a synonym for `additionalProperties`/`items`. Whatever it cannot answer it declines at compile time or bails from at runtime, and the parse path takes over unchanged. `tests/test_scanner_differential.js` holds the scanner and `validate()` to the same verdict on the same text: 413,130 comparisons over the official suite, a malformed-JSON corpus and generated corruptions, zero disagreements, and the count must stay at zero.
|
|
10
|
+
- Cross-process medians against the same call forced through `JSON.parse`: accepted documents 1.19x to 1.43x by size; rejection at the first element 37x at 4 KB and 385x at 41 KB.
|
|
11
|
+
- Generated config schemas are first-class: up to 4096 properties per node, and past 48 names the dispatch is a rolling hash accumulated during the key scan, one lookup in a Map passed outside the source, one `startsWith` to confirm, bodies shared per distinct subschema, and an in-order fast path for machine-written JSON that resynchronises across omitted optional keys. A 26.6 KB config with 1300 declared properties: 136.7 µs by parse-and-validate, 43.8 µs by scan.
|
|
12
|
+
- `isValidJSON` also remembers the last (text, verdict) pair, since the verdict is a pure function of the text: a drift monitor re-reading an unchanged file answers in 0.44 µs by native string compare. Withheld when user formats or custom keywords are present, since user functions may not be pure.
|
|
13
|
+
- Strict mode covers all four cases from issue #44: unknown keywords (with a spelling suggestion), unresolvable local `$ref`s, keywords the node's own `type` makes inert (`minimum` on a string can never fire), and `required` names nothing can satisfy (`additionalProperties: false` with the name absent from `properties`). `ata compile --strict-schema` and `build({ strictSchema: true })` run the same checks at build time and refuse to emit a module from a schema that fails them.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- An AOT module that cannot carry error detail says so. `toStandaloneModule` with error detail requested builds the error function from the error generator, which declines some schemas, `unevaluated*` among them; the runtime validator falls back to the interpreted engine there, but a standalone module has nothing to fall back to, so it shipped with an exact verdict and a single ATA9000 stub for every failure, silently. The module still ships, since the verdict is exact and validating the rare failing document with the runtime `Validator` is a legitimate pattern, but the degradation is loud in every layer: `onWarning` on the emitter, a NOTE in the module header, a `warnings` list in the `build()` report, a printed warning from `ata compile`, and a refused build under `--strict`. Found by a gateway team that lost their per-field startup errors to it.
|
|
18
|
+
|
|
19
|
+
## 1.23.0 - 2026-09-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `strictSchema: true | 'log'`, and the compat shim's `strict`/`strictSchema` now enforce it: the authoring-time checks for the mistakes that fail open. An unknown keyword throws at construction with its path and the nearest known spelling, `strict mode: unknown keyword "maxLenght" (did you mean "maxLength"?) at #/properties/a/maxLenght`, and a local `$ref` that does not resolve in its own document throws instead of compiling a validator that rejects everything. `'log'` warns through the logger and continues. Off by default. The known-keyword list is derived from the vendored meta-schema documents rather than maintained by hand, plus the keywords ata implements beyond them; `x-` prefixed names and keywords registered through the `keywords` option pass. Property names, `enum` and `const` values, `default` and `examples` are data, not keywords, and the walk knows the difference, including the draft-07 `dependencies` form. `strictTypes`, `strictTuples` and `strictRequired` remain accepted and ignored, and `compat.d.ts` now says which is which. Reported as issue #44 by a migration that fuzzed 1,364 mutations against both validators, found zero divergence, and then shipped a typo.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- The error and combined generators compile on the first rejection instead of at startup. Every schema compiled three functions eagerly: the verdict, the combined validate-and-collect form, and the error generator, which on a cold first call cost 8.2, 10.4 and 7.8 ms on a 120-property config schema, most of it V8 compiling each generator's own code the first time it is entered. A caller that never reads an error never needs the last two, and `validate()` on a passing document does not either, so they are built on the first rejection. Compile plus first verdict on that schema: 30.7 to 10.8 ms, interleaved runs of committed builds. The warm path measured the same on all ten cases checked. The shared compile cache keeps `undefined` meaning not built yet and `null` meaning the compiler declined, in both places that seed it, and `tests/test_compile_cache_order.js` caught the one seam where the two seeders disagreed during this change.
|
|
28
|
+
- The static `unevaluatedProperties` tier stopped checking membership with a first-character switch. Keys sharing a first character, `ENV_0` through `ENV_999` say, all land in one case whose body chains every name, per key of the document: the `additionalProperties` quadratic in a different coat. Above 128 declared names the generated code reads the hoisted lookup that fixed the first one; below that the switch stays. A 1300-property config document with both of its objects closed: 87.7 to 24.5 µs, and the keyword now costs what `additionalProperties` costs, which is the membership test itself. Only the boolean generator has this code; the error and combined generators decline `unevaluated*` and the interpreted engine produces those errors.
|
|
29
|
+
- The compat shim derives each error's pointer segments and kinds once, into a key map shared by the reference-order sort and the shaping loop. The sort comparator used to re-split and re-classify both errors' pointers on every one of its comparisons, and the loop derived the same segments again per error, with the `if` wrapper's flush doing it a third time. A seven-error rejection through `ata-validator/compat` with `allErrors`: 5.79 to 3.27 µs. The shim's own overhead over `Validator.validate` fell from 4.4 µs to 1.9. The compat surface is pinned by `test_compat`, 106 error-format cases, 36 parity cases and the error-shape differential, all green before and after.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- The API table said `verbose` attaches only `parentSchema`; it attaches `parentSchema`, `schema` and `data`, and the migration guide now lists which `params` key each keyword carries. The performance notes recorded "length-bucketed key comparison for `additionalProperties: false`" as measured and dropped on the grounds that each comparison is a pointer compare; that was right about one comparison and wrong about the total, and the note now carries the measurements that replaced it.
|
|
34
|
+
|
|
5
35
|
## 1.22.1 - 2026-09-16
|
|
6
36
|
|
|
7
37
|
### Fixed
|
package/bin/ata.js
CHANGED
|
@@ -38,6 +38,10 @@ Build options:
|
|
|
38
38
|
--cache-file <path> Cache file for incremental builds (default: cache disabled)
|
|
39
39
|
--max-size <bytes> Fail build if any compiled module exceeds this gzipped size
|
|
40
40
|
--strict Treat any AOT-incompatible schema as a build error (default: skip + warn)
|
|
41
|
+
--strict-schema Fail the build on schema authoring mistakes: unknown
|
|
42
|
+
keywords (with a spelling suggestion), keywords the
|
|
43
|
+
node's type makes inert, unresolvable local $refs,
|
|
44
|
+
and required names nothing can satisfy
|
|
41
45
|
--watch Re-emit on schema change (Ctrl-C to exit)
|
|
42
46
|
--no-types Skip .d.mts/.d.cts emission alongside compiled modules
|
|
43
47
|
--source Embed schema source map (default in development)
|
|
@@ -80,6 +84,7 @@ function parseArgs(argv) {
|
|
|
80
84
|
if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
|
|
81
85
|
if (a === '--check') { out.opts.check = true; continue; }
|
|
82
86
|
if (a === '--strict') { out.opts.strict = true; continue; }
|
|
87
|
+
if (a === '--strict-schema') { out.opts.strictSchema = true; continue; }
|
|
83
88
|
if (a === '--out-dir') { out.opts.outDir = argv[++i]; continue; }
|
|
84
89
|
if (a === '--suffix') { out.opts.suffix = argv[++i]; continue; }
|
|
85
90
|
if (a === '--cache-file') { out.opts.cacheFile = argv[++i]; continue; }
|
|
@@ -180,6 +185,19 @@ function cmdCompile(args) {
|
|
|
180
185
|
process.exit(1);
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
// The build is where an authoring mistake is cheapest to stop: a typo like
|
|
189
|
+
// maxLenght compiles into a module that simply lacks the rule, and nobody
|
|
190
|
+
// revisits a compiled module. Findings fail the compile, with the path and
|
|
191
|
+
// the suggested spelling.
|
|
192
|
+
if (args.opts.strictSchema) {
|
|
193
|
+
const { checkSchemaStrict } = require('../lib/strict-check');
|
|
194
|
+
const problems = checkSchemaStrict(schema, {});
|
|
195
|
+
if (problems.length > 0) {
|
|
196
|
+
for (const x of problems) reportCompileError(input, `strict mode: ${x.message} at ${x.path}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
183
201
|
const { Validator } = require('..');
|
|
184
202
|
const aot = require('../lib/aot');
|
|
185
203
|
let v;
|
|
@@ -200,7 +218,15 @@ function cmdCompile(args) {
|
|
|
200
218
|
}
|
|
201
219
|
}
|
|
202
220
|
const schemaFile = path.relative(process.cwd(), input) || input;
|
|
203
|
-
const
|
|
221
|
+
const compileWarnings = [];
|
|
222
|
+
const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile, onWarning: (w) => compileWarnings.push(w) });
|
|
223
|
+
if (compileWarnings.length > 0) {
|
|
224
|
+
if (args.opts.strict) {
|
|
225
|
+
for (const w of compileWarnings) reportCompileError(input, w);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${w}\n`);
|
|
229
|
+
}
|
|
204
230
|
if (!src) {
|
|
205
231
|
reportCompileError(input, 'schema is too complex for standalone compilation');
|
|
206
232
|
process.exit(1);
|
package/compat.d.ts
CHANGED
|
@@ -78,10 +78,15 @@ declare namespace Ata {
|
|
|
78
78
|
formats?: Record<string, Format>;
|
|
79
79
|
keywords?: KeywordDefinition[];
|
|
80
80
|
schemas?: object[] | Record<string, object>;
|
|
81
|
+
/** Enforced: unknown keywords and dangling local $refs throw at compile ('log' warns instead). */
|
|
81
82
|
strict?: boolean | 'log';
|
|
83
|
+
/** Same checks as `strict`; the specific option wins over the umbrella one. */
|
|
82
84
|
strictSchema?: boolean | 'log';
|
|
85
|
+
/** Accepted for API compatibility; not enforced. */
|
|
83
86
|
strictTypes?: boolean | 'log';
|
|
87
|
+
/** Accepted for API compatibility; not enforced. */
|
|
84
88
|
strictTuples?: boolean | 'log';
|
|
89
|
+
/** Accepted for API compatibility; not enforced. */
|
|
85
90
|
strictRequired?: boolean | 'log';
|
|
86
91
|
allowUnionTypes?: boolean;
|
|
87
92
|
logger?: { log(...args: unknown[]): void; warn(...args: unknown[]): void; error(...args: unknown[]): void } | false;
|
package/compat.js
CHANGED
|
@@ -15,8 +15,10 @@ const { resolvePointer } = require('./lib/pointer.js');
|
|
|
15
15
|
// - `$data` references;
|
|
16
16
|
// - keywords defined only through `code` (a code generator hook);
|
|
17
17
|
// - the reference formats plugin, whose formats are built in here.
|
|
18
|
-
// Strict-mode schema checks
|
|
19
|
-
//
|
|
18
|
+
// Strict-mode schema checks: `strict` and `strictSchema` are enforced for the
|
|
19
|
+
// checks that fail open, an unknown keyword (`maxLenght` compiles and the
|
|
20
|
+
// constraint is simply absent) and a dangling local `$ref`. `strictTypes`,
|
|
21
|
+
// `strictTuples` and `strictRequired` are accepted and ignored.
|
|
20
22
|
|
|
21
23
|
const { Validator } = require('./index');
|
|
22
24
|
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
@@ -99,6 +101,13 @@ class Ata {
|
|
|
99
101
|
removeAdditional: !!o.removeAdditional,
|
|
100
102
|
verbose: !!o.verbose,
|
|
101
103
|
};
|
|
104
|
+
// `strict` and `strictSchema`: the unknown-keyword and dangling-local-$ref
|
|
105
|
+
// checks are enforced; `strictTypes`, `strictTuples` and `strictRequired`
|
|
106
|
+
// are still accepted and ignored. The specific keyword option wins over
|
|
107
|
+
// the umbrella one, which is how the reference class reads them.
|
|
108
|
+
const strictness = o.strictSchema !== undefined ? o.strictSchema : o.strict;
|
|
109
|
+
if (strictness === true || strictness === 'log') out.strictSchema = strictness;
|
|
110
|
+
if (o.logger !== undefined) out.logger = o.logger;
|
|
102
111
|
if (o.validateFormats === false) out.assertFormat = false;
|
|
103
112
|
const formatNames = Object.keys(this._formats);
|
|
104
113
|
if (formatNames.length > 0) out.formats = { ...this._formats };
|
package/index.js
CHANGED
|
@@ -885,6 +885,29 @@ class Validator {
|
|
|
885
885
|
// produced the error). Matches ajv's `verbose: true` behavior.
|
|
886
886
|
this._verbose = !!options.verbose;
|
|
887
887
|
|
|
888
|
+
// strictSchema: authoring-time checks, off by default. A mistyped keyword
|
|
889
|
+
// is the one schema mistake that fails open: to every dialect `maxLenght`
|
|
890
|
+
// is an annotation, so the constraint the author meant is simply absent
|
|
891
|
+
// and previously invalid data validates. `true` throws here, at
|
|
892
|
+
// construction, with every finding; `'log'` reports through
|
|
893
|
+
// `options.logger` or the console and continues. The check runs on the
|
|
894
|
+
// schema as written, before any normalization touches it.
|
|
895
|
+
if (options.strictSchema === true || options.strictSchema === 'log') {
|
|
896
|
+
const { checkSchemaStrict } = require('./lib/strict-check');
|
|
897
|
+
const problems = checkSchemaStrict(schema, { userKeywords: options.keywords || null });
|
|
898
|
+
if (problems.length > 0) {
|
|
899
|
+
const text = problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n');
|
|
900
|
+
if (options.strictSchema === true) {
|
|
901
|
+
throw new Error(text);
|
|
902
|
+
}
|
|
903
|
+
const logger = options.logger;
|
|
904
|
+
if (logger !== false) {
|
|
905
|
+
const warn = logger && typeof logger.warn === 'function' ? logger.warn.bind(logger) : console.warn;
|
|
906
|
+
warn(text);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
888
911
|
// richErrors: default true. Only the literal `false` opts back into the
|
|
889
912
|
// v0.14 error shape (no code/expected/received/docUrl, no aliases).
|
|
890
913
|
this._richErrors = options && options.richErrors === false ? false : true;
|
|
@@ -910,6 +933,8 @@ class Validator {
|
|
|
910
933
|
// to attach dataFrame entries to each enriched error.
|
|
911
934
|
this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
|
|
912
935
|
this._lastRawInput = null;
|
|
936
|
+
// undefined: not built yet. null: this schema has no scanner.
|
|
937
|
+
this._scanner = undefined;
|
|
913
938
|
|
|
914
939
|
// Public methods start as memoized accessors on the prototype; nothing is
|
|
915
940
|
// allocated per instance until one is first read. See _defineLazyMethod
|
|
@@ -1014,27 +1039,40 @@ class Validator {
|
|
|
1014
1039
|
// because nothing has tried to build them yet. Both halves of that
|
|
1015
1040
|
// distinction are null, and reading the second as the first costs this
|
|
1016
1041
|
// schema its generated error function for the life of the process.
|
|
1017
|
-
} else if (cached && cached.
|
|
1042
|
+
} else if (cached && cached.jsFn !== undefined && !_forceNapi) {
|
|
1043
|
+
// `full` says the error and combined functions exist too. An entry
|
|
1044
|
+
// without it still carries a verdict function worth reusing; the pair is
|
|
1045
|
+
// built by _buildDeferred below if something asks for an error, and the
|
|
1046
|
+
// entry is upgraded then. `undefined` in `combined`/`errFn` means not
|
|
1047
|
+
// built yet; `null` means the compiler declined. Those two must never
|
|
1048
|
+
// blur: reading the first as the second is the bug this cache had once
|
|
1049
|
+
// already, and it silently cost schemas their generated error function.
|
|
1018
1050
|
jsFn = cached.jsFn;
|
|
1019
1051
|
jsCombinedFn = cached.combined;
|
|
1020
1052
|
jsErrFn = cached.errFn;
|
|
1021
1053
|
_isCodegen = !!cached.isCodegen;
|
|
1054
|
+
this._engine = _isCodegen ? 'codegen' : jsFn ? 'closure' : null;
|
|
1022
1055
|
} else if (!_forceNapi) {
|
|
1023
1056
|
const uf = this._userFormats;
|
|
1024
1057
|
const _cgFn = compileToJSCodegen(schemaObj, sm, uf);
|
|
1025
1058
|
jsFn = _cgFn || compileToJS(schemaObj, null, sm);
|
|
1026
|
-
|
|
1027
|
-
|
|
1059
|
+
// Only the verdict is compiled here. The error and combined generators
|
|
1060
|
+
// are the other two thirds of a cold first call (8.2, 10.4 and 7.8 ms on
|
|
1061
|
+
// a 120-property config schema, most of it V8 compiling each generator
|
|
1062
|
+
// the first time it is entered), and a caller that never reads an error
|
|
1063
|
+
// never needs them. _buildDeferred compiles them on the first rejection.
|
|
1064
|
+
jsCombinedFn = undefined;
|
|
1065
|
+
jsErrFn = undefined;
|
|
1028
1066
|
_isCodegen = !!_cgFn;
|
|
1029
1067
|
this._engine = _cgFn ? 'codegen' : jsFn ? 'closure' : null;
|
|
1030
1068
|
if (!uf) {
|
|
1031
|
-
_compileCache.set(mapKey, { jsFn, combined:
|
|
1069
|
+
_compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
|
|
1032
1070
|
}
|
|
1033
1071
|
} else {
|
|
1034
1072
|
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
1035
1073
|
}
|
|
1036
1074
|
this._jsFn = jsFn;
|
|
1037
|
-
if (this._engine === undefined) this._engine =
|
|
1075
|
+
if (this._engine === undefined) this._engine = null;
|
|
1038
1076
|
|
|
1039
1077
|
// Data mutators -- try codegen first (12x faster), fallback to closure arrays.
|
|
1040
1078
|
// Follow cross-refs so coercion/defaults/removeAdditional see the referenced
|
|
@@ -1080,14 +1118,26 @@ class Validator {
|
|
|
1080
1118
|
)));
|
|
1081
1119
|
const useSimdjsonForLarge = !hasArrayTraversal;
|
|
1082
1120
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1121
|
+
// Builds the two generators the compile step left out, once, and upgrades
|
|
1122
|
+
// the shared cache entry. `undefined` means not built yet; `null` means the
|
|
1123
|
+
// compiler declined. Conflating those is what once cost every schema its
|
|
1124
|
+
// generated error function for the life of the process, so they stay apart.
|
|
1125
|
+
const _buildDeferred = () => {
|
|
1126
|
+
if (jsCombinedFn !== undefined && jsErrFn !== undefined) return;
|
|
1127
|
+
const uf2 = this._userFormats;
|
|
1128
|
+
if (jsCombinedFn === undefined) jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, uf2) || null;
|
|
1129
|
+
if (jsErrFn === undefined) jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, uf2) || null;
|
|
1130
|
+
if (!uf2) {
|
|
1131
|
+
const entry = _compileCache.get(mapKey);
|
|
1132
|
+
if (entry && entry.jsFn === jsFn) {
|
|
1133
|
+
entry.combined = jsCombinedFn;
|
|
1134
|
+
entry.errFn = jsErrFn;
|
|
1135
|
+
entry.full = true;
|
|
1136
|
+
}
|
|
1090
1137
|
}
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
if (jsFn) {
|
|
1091
1141
|
// errFn: use JS codegen if safe, else native fallback (only when native
|
|
1092
1142
|
// is available). Environments without the native addon — Cloudflare
|
|
1093
1143
|
// Workers, browser, Bun without N-API — get a JS-only fallback so the
|
|
@@ -1126,19 +1176,38 @@ class Validator {
|
|
|
1126
1176
|
// reports those schemas correctly, so failing data is re-validated
|
|
1127
1177
|
// there. This used to be a placeholder error with no keyword and no
|
|
1128
1178
|
// path, which hid whatever had actually failed.
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1179
|
+
// Resolved on the first rejection rather than at compile time, because
|
|
1180
|
+
// building the generator behind it is two thirds of what a first call
|
|
1181
|
+
// costs and a caller that never reads an error never needs it. The probe
|
|
1182
|
+
// moves here with it: it calls the generated function, so it cannot run
|
|
1183
|
+
// before the function exists.
|
|
1184
|
+
let _errOnlyImpl = null;
|
|
1185
|
+
const errOnly = (d) => {
|
|
1186
|
+
if (_errOnlyImpl === null) {
|
|
1187
|
+
_buildDeferred();
|
|
1188
|
+
let safe = null;
|
|
1189
|
+
if (jsErrFn) {
|
|
1190
|
+
try {
|
|
1191
|
+
jsErrFn({}, true);
|
|
1192
|
+
safe = (x) => jsErrFn(x, true);
|
|
1193
|
+
} catch {}
|
|
1194
|
+
}
|
|
1195
|
+
_errOnlyImpl =
|
|
1196
|
+
safe ||
|
|
1197
|
+
(hasUnevaluated || !native
|
|
1198
|
+
? jsOnlyFallback
|
|
1199
|
+
: hasDynRef
|
|
1200
|
+
? (x) => {
|
|
1201
|
+
this._ensureNative();
|
|
1202
|
+
return this._compiled.validateJSON(JSON.stringify(x));
|
|
1203
|
+
}
|
|
1204
|
+
: (x) => {
|
|
1205
|
+
this._ensureNative();
|
|
1206
|
+
return this._compiled.validate(x);
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return _errOnlyImpl(d);
|
|
1210
|
+
};
|
|
1142
1211
|
|
|
1143
1212
|
// Best path: combined validator (single pass, validates + collects errors)
|
|
1144
1213
|
// Valid data: returns VALID_RESULT, no allocation
|
|
@@ -1148,24 +1217,41 @@ class Validator {
|
|
|
1148
1217
|
// Test combined at compile time -- some schemas (e.g. if/then/else)
|
|
1149
1218
|
// produce broken combined code that crashes on certain inputs.
|
|
1150
1219
|
// We probe with diverse data; if any throws, fall back to hybrid.
|
|
1151
|
-
let
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1220
|
+
let _combinedProbed = false;
|
|
1221
|
+
let _safeCombined = null;
|
|
1222
|
+
const combinedIfSafe = () => {
|
|
1223
|
+
if (_combinedProbed) return _safeCombined;
|
|
1224
|
+
_combinedProbed = true;
|
|
1225
|
+
_buildDeferred();
|
|
1226
|
+
if (jsCombinedFn) {
|
|
1227
|
+
try {
|
|
1228
|
+
const probe = {};
|
|
1229
|
+
// Populate probe with one key per known property to trigger nested paths
|
|
1230
|
+
if (schemaObj && schemaObj.properties) {
|
|
1231
|
+
for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
|
|
1232
|
+
}
|
|
1233
|
+
if (schemaObj && schemaObj.if && schemaObj.if.properties) {
|
|
1234
|
+
for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
|
|
1235
|
+
}
|
|
1236
|
+
jsCombinedFn(probe);
|
|
1237
|
+
jsCombinedFn({});
|
|
1238
|
+
jsCombinedFn(null);
|
|
1239
|
+
jsCombinedFn(0);
|
|
1240
|
+
_safeCombined = jsCombinedFn;
|
|
1241
|
+
} catch {}
|
|
1242
|
+
}
|
|
1243
|
+
return _safeCombined;
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
// What the hybrid path hands to its error slot: the combined function
|
|
1247
|
+
// when it is usable, since it validates and collects in one pass, and
|
|
1248
|
+
// the error generator otherwise. Same order the eager code chose, just
|
|
1249
|
+
// chosen on the first rejection.
|
|
1250
|
+
let _errPreferredImpl = null;
|
|
1251
|
+
const errPreferCombined = (d) => {
|
|
1252
|
+
if (_errPreferredImpl === null) _errPreferredImpl = combinedIfSafe() || errOnly;
|
|
1253
|
+
return _errPreferredImpl(d);
|
|
1254
|
+
};
|
|
1169
1255
|
|
|
1170
1256
|
// The boolean engine is the verdict authority for these paths; the
|
|
1171
1257
|
// final lazy wrapper uses it to skip error construction entirely.
|
|
@@ -1182,7 +1268,7 @@ class Validator {
|
|
|
1182
1268
|
: (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
|
|
1183
1269
|
} else if (hasDynRef && _isCodegen && jsFn) {
|
|
1184
1270
|
// $dynamicRef with JS codegen: direct path, no wrapper layers
|
|
1185
|
-
const _fn = jsFn, _efn =
|
|
1271
|
+
const _fn = jsFn, _efn = errOnly, _R = VALID_RESULT;
|
|
1186
1272
|
this.validate = preprocess
|
|
1187
1273
|
? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
|
|
1188
1274
|
: (data) => _fn(data) ? _R : _efn(data);
|
|
@@ -1207,31 +1293,29 @@ class Validator {
|
|
|
1207
1293
|
} else if (jsFn && jsFn._hybridFactory) {
|
|
1208
1294
|
// Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
|
|
1209
1295
|
// No arrow function wrapper, no ternary, one function call
|
|
1210
|
-
|
|
1296
|
+
// The factory bakes the error function in as an argument and never
|
|
1297
|
+
// calls it for a document that passes, so a resolver here costs the
|
|
1298
|
+
// accepted path nothing and keeps the compile off the first call.
|
|
1299
|
+
const hybridFn = jsFn._hybridFactory(VALID_RESULT, errPreferCombined);
|
|
1211
1300
|
this.validate = preprocess
|
|
1212
1301
|
? (data) => { preprocess(data); return hybridFn(data); }
|
|
1213
1302
|
: hybridFn;
|
|
1214
|
-
} else if (safeCombinedFn) {
|
|
1215
|
-
this.validate = preprocess
|
|
1216
|
-
? (data) => { preprocess(data); return safeCombinedFn(data); }
|
|
1217
|
-
: safeCombinedFn;
|
|
1218
1303
|
} else {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
? (data) => {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
: hybridFn
|
|
1229
|
-
: preprocess
|
|
1304
|
+
// No hybrid factory, so the assembly needs the function itself rather
|
|
1305
|
+
// than a reference it can call later: build it now.
|
|
1306
|
+
const safeCombinedFn = combinedIfSafe();
|
|
1307
|
+
if (safeCombinedFn) {
|
|
1308
|
+
this.validate = preprocess
|
|
1309
|
+
? (data) => { preprocess(data); return safeCombinedFn(data); }
|
|
1310
|
+
: safeCombinedFn;
|
|
1311
|
+
} else {
|
|
1312
|
+
this.validate = preprocess
|
|
1230
1313
|
? (data) => {
|
|
1231
1314
|
preprocess(data);
|
|
1232
|
-
return jsFn(data) ? VALID_RESULT :
|
|
1315
|
+
return jsFn(data) ? VALID_RESULT : errOnly(data);
|
|
1233
1316
|
}
|
|
1234
|
-
: (data) => (jsFn(data) ? VALID_RESULT :
|
|
1317
|
+
: (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
|
|
1318
|
+
}
|
|
1235
1319
|
}
|
|
1236
1320
|
// Verbose mode: populate parentSchema, schema and data on each error, the
|
|
1237
1321
|
// three fields the default error shape carries under the same option.
|
|
@@ -1274,12 +1358,15 @@ class Validator {
|
|
|
1274
1358
|
this.isValidObject = preprocess
|
|
1275
1359
|
? (data) => { preprocess(data); return jsFn(data) }
|
|
1276
1360
|
: jsFn;
|
|
1361
|
+
// Same preference as the object path: the combined function first, since
|
|
1362
|
+
// it validates and collects in one pass, and the error generator behind
|
|
1363
|
+
// it. `errPreferCombined` is that order, resolved on the first rejection
|
|
1364
|
+
// instead of at compile time.
|
|
1277
1365
|
const hybridFn = jsFn._hybridFactory
|
|
1278
|
-
? jsFn._hybridFactory(VALID_RESULT,
|
|
1366
|
+
? jsFn._hybridFactory(VALID_RESULT, errPreferCombined)
|
|
1279
1367
|
: null;
|
|
1280
|
-
const jsonValidateInner =
|
|
1281
|
-
||
|
|
1282
|
-
|| ((obj) => (jsFn(obj) ? VALID_RESULT : errFn(obj)));
|
|
1368
|
+
const jsonValidateInner = hybridFn
|
|
1369
|
+
|| ((obj) => (jsFn(obj) ? VALID_RESULT : errPreferCombined(obj)));
|
|
1283
1370
|
// Parsed text takes the same preprocess pass as a parsed object, so
|
|
1284
1371
|
// validate(obj) and validateJSON(text) answer the same for the same
|
|
1285
1372
|
// document. Without it, coercion, removal and defaults applied on one
|
|
@@ -1341,6 +1428,23 @@ class Validator {
|
|
|
1341
1428
|
return verdictFromText(jsonStr);
|
|
1342
1429
|
}
|
|
1343
1430
|
: verdictFromText;
|
|
1431
|
+
|
|
1432
|
+
// A schema-directed scanner answers the verdict from the JSON text
|
|
1433
|
+
// without building the document. Parsing is around three quarters of the
|
|
1434
|
+
// cost of a real request, and a caller that only wants yes or no should
|
|
1435
|
+
// not pay it; a rejection can also stop at the byte that caused it
|
|
1436
|
+
// instead of parsing the rest of a document that is already refused.
|
|
1437
|
+
//
|
|
1438
|
+
// It is wired only where the verdict IS the answer. On a path that has
|
|
1439
|
+
// to produce errors, scanning an invalid document is work thrown away,
|
|
1440
|
+
// so those keep parsing. `abortEarly` has no errors to produce, so it
|
|
1441
|
+
// counts as a verdict path.
|
|
1442
|
+
//
|
|
1443
|
+
// Not wired when a preprocess pass is configured: coercion, removal and
|
|
1444
|
+
// defaults rewrite the document before it is judged, and the scanner
|
|
1445
|
+
// reads what arrived. The compiler declines any schema it cannot answer
|
|
1446
|
+
// and a compiled scanner returns BAIL for a document shape it cannot
|
|
1447
|
+
// answer, and then the parse path below takes over unchanged.
|
|
1344
1448
|
// validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
|
|
1345
1449
|
// validate) so it works with or without the native addon and in browsers.
|
|
1346
1450
|
{
|
|
@@ -1714,6 +1818,83 @@ class Validator {
|
|
|
1714
1818
|
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
|
|
1715
1819
|
if (bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords)) installSlowBufferApis(this);
|
|
1716
1820
|
}
|
|
1821
|
+
// Installed after the buffer gate on purpose: the gate replaces
|
|
1822
|
+
// isValidJSON for schemas whose shapes the native walker gets wrong,
|
|
1823
|
+
// unevaluatedProperties among them, and the scanner wiring has to wrap
|
|
1824
|
+
// whatever answers last or a gated schema silently loses its scanner.
|
|
1825
|
+
if (this._jsFn && !this._preprocess) {
|
|
1826
|
+
const self = this;
|
|
1827
|
+
// Generating a scanner costs about 20 microseconds, measured, and it
|
|
1828
|
+
// saves from around 85 nanoseconds on a small accepted document to
|
|
1829
|
+
// several microseconds on a rejected one. Building it on the first
|
|
1830
|
+
// call would therefore be a straight loss for a caller that checks one
|
|
1831
|
+
// document and exits, so it is built once a caller has asked often
|
|
1832
|
+
// enough that it is plainly doing this in a loop. A server passes the
|
|
1833
|
+
// line during warm-up and never sees it.
|
|
1834
|
+
const SCAN_AFTER = 64;
|
|
1835
|
+
let calls = 0;
|
|
1836
|
+
// undefined: not built. null: this schema has no scanner. Passing true
|
|
1837
|
+
// builds it now, which is how the differential test reaches it.
|
|
1838
|
+
this._ensureScanner = (now) => {
|
|
1839
|
+
if (self._scanner === undefined) {
|
|
1840
|
+
if (!now && ++calls < SCAN_AFTER) return undefined;
|
|
1841
|
+
const built = require('./lib/scan-compiler').compileScanner(schemaObj, { userFormats: self._userFormats });
|
|
1842
|
+
self._scanner = built ? built.scan : null;
|
|
1843
|
+
}
|
|
1844
|
+
return self._scanner;
|
|
1845
|
+
};
|
|
1846
|
+
const byParsing = this.isValidJSON;
|
|
1847
|
+
// The verdict is a pure function of the text, and the caller a
|
|
1848
|
+
// gateway or a drift monitor keeps asking about is usually the same
|
|
1849
|
+
// text: a config file re-read on a timer, a heartbeat body. One
|
|
1850
|
+
// remembered (text, verdict) pair answers that case with a native
|
|
1851
|
+
// string compare, which is a memcmp, instead of a scan. Withheld when
|
|
1852
|
+
// user formats or custom keywords are present, since those are user
|
|
1853
|
+
// functions and nothing guarantees they are pure.
|
|
1854
|
+
const memoizable = !self._userFormats && !self._usesKeywords;
|
|
1855
|
+
let _memoText = null;
|
|
1856
|
+
let _memoVerdict = false;
|
|
1857
|
+
this.isValidJSON = (jsonStr) => {
|
|
1858
|
+
const scan = self._ensureScanner();
|
|
1859
|
+
if (scan === undefined) return byParsing(jsonStr);
|
|
1860
|
+
if (scan === null) { self.isValidJSON = byParsing; return byParsing(jsonStr); }
|
|
1861
|
+
self.isValidJSON = memoizable
|
|
1862
|
+
? (text) => {
|
|
1863
|
+
if (typeof text !== 'string') return byParsing(text);
|
|
1864
|
+
if (text === _memoText) return _memoVerdict;
|
|
1865
|
+
const r = scan(text);
|
|
1866
|
+
const verdict = r === -1 ? byParsing(text) : r === 1;
|
|
1867
|
+
_memoText = text;
|
|
1868
|
+
_memoVerdict = verdict;
|
|
1869
|
+
return verdict;
|
|
1870
|
+
}
|
|
1871
|
+
: (text) => {
|
|
1872
|
+
if (typeof text !== 'string') return byParsing(text);
|
|
1873
|
+
const r = scan(text);
|
|
1874
|
+
if (r === -1) return byParsing(text);
|
|
1875
|
+
return r === 1;
|
|
1876
|
+
};
|
|
1877
|
+
return self.isValidJSON(jsonStr);
|
|
1878
|
+
};
|
|
1879
|
+
if (options.abortEarly) {
|
|
1880
|
+
const validateByParsing = this.validateJSON;
|
|
1881
|
+
this.validateJSON = (jsonStr) => {
|
|
1882
|
+
const scan = self._ensureScanner();
|
|
1883
|
+
if (scan === undefined) return validateByParsing(jsonStr);
|
|
1884
|
+
if (scan === null) { self.validateJSON = validateByParsing; return validateByParsing(jsonStr); }
|
|
1885
|
+
self.validateJSON = (text) => {
|
|
1886
|
+
if (typeof text === 'string') {
|
|
1887
|
+
const r = scan(text);
|
|
1888
|
+
if (r === 1) return VALID_RESULT;
|
|
1889
|
+
if (r === 0) return ABORT_EARLY_RESULT;
|
|
1890
|
+
}
|
|
1891
|
+
return validateByParsing(text);
|
|
1892
|
+
};
|
|
1893
|
+
return self.validateJSON(jsonStr);
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1717
1898
|
|
|
1718
1899
|
// Save to identity cache for ultra-fast reuse with same schema object.
|
|
1719
1900
|
// Only an instance built without options may answer a later
|
|
@@ -1799,15 +1980,20 @@ class Validator {
|
|
|
1799
1980
|
return;
|
|
1800
1981
|
}
|
|
1801
1982
|
const uf = this._userFormats;
|
|
1802
|
-
const
|
|
1983
|
+
const _cg = compileToJSCodegen(this._schemaObj, sm, uf);
|
|
1984
|
+
const jsFn = _cg || compileToJS(this._schemaObj, null, sm);
|
|
1803
1985
|
this._jsFn = jsFn;
|
|
1804
1986
|
if (jsFn) {
|
|
1805
1987
|
this.isValidObject = jsFn;
|
|
1806
1988
|
// A partial entry: the verdict function is real, the other two are not
|
|
1807
|
-
// built yet rather than declined. `
|
|
1808
|
-
//
|
|
1989
|
+
// built yet rather than declined. `undefined` is the not-built marker
|
|
1990
|
+
// the full compile's _buildDeferred looks for; `null` would read as
|
|
1991
|
+
// "the compiler declined" and cost the schema its error function, which
|
|
1992
|
+
// is the bug this cache had once already. `isCodegen` rides along so a
|
|
1993
|
+
// validator that later reuses this entry reports the same engine it
|
|
1994
|
+
// would have compiled to.
|
|
1809
1995
|
if (!uf) {
|
|
1810
|
-
if (!cached) _compileCache.set(mapKey, { jsFn, combined:
|
|
1996
|
+
if (!cached) _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
|
|
1811
1997
|
else cached.jsFn = jsFn;
|
|
1812
1998
|
}
|
|
1813
1999
|
}
|
package/lib/aot-build.js
CHANGED
|
@@ -94,6 +94,7 @@ async function build(opts) {
|
|
|
94
94
|
const cached = [];
|
|
95
95
|
const skipped = [];
|
|
96
96
|
const failed = [];
|
|
97
|
+
const warnings = [];
|
|
97
98
|
|
|
98
99
|
for (const input of inputs) {
|
|
99
100
|
try {
|
|
@@ -126,6 +127,18 @@ async function build(opts) {
|
|
|
126
127
|
continue;
|
|
127
128
|
}
|
|
128
129
|
const schema = parseSchemaFile(input);
|
|
130
|
+
// The build is where an authoring mistake is cheapest to stop: a typo
|
|
131
|
+
// like maxLenght compiles into a validator that simply lacks the rule,
|
|
132
|
+
// and a compiled module fails even more quietly than a runtime one,
|
|
133
|
+
// since nobody ever revisits it. Findings fail the input's build.
|
|
134
|
+
if (opts.strictSchema) {
|
|
135
|
+
const { checkSchemaStrict } = require('./strict-check');
|
|
136
|
+
const problems = checkSchemaStrict(schema, {});
|
|
137
|
+
if (problems.length > 0) {
|
|
138
|
+
failed.push({ input, error: problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('; ') });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
129
142
|
const v = new Validator(schema);
|
|
130
143
|
const source = resolveSourceDefault(opts);
|
|
131
144
|
let sourceMap = null;
|
|
@@ -138,6 +151,7 @@ async function build(opts) {
|
|
|
138
151
|
}
|
|
139
152
|
}
|
|
140
153
|
const schemaFile = path.relative(process.cwd(), input) || input;
|
|
154
|
+
const inputWarnings = [];
|
|
141
155
|
const src = aot.toStandaloneModule(v, {
|
|
142
156
|
format,
|
|
143
157
|
abortEarly: !!opts.abortEarly,
|
|
@@ -145,7 +159,15 @@ async function build(opts) {
|
|
|
145
159
|
sourceMap,
|
|
146
160
|
schemaFile,
|
|
147
161
|
formatMode: opts.formatMode,
|
|
162
|
+
onWarning: (w) => inputWarnings.push(w),
|
|
148
163
|
});
|
|
164
|
+
if (inputWarnings.length > 0) {
|
|
165
|
+
if (opts.strict) {
|
|
166
|
+
failed.push({ input, error: inputWarnings.join('; ') });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const w of inputWarnings) warnings.push({ input, warning: w });
|
|
170
|
+
}
|
|
149
171
|
if (!src) {
|
|
150
172
|
const reason = 'schema is not AOT-compatible (toStandaloneModule returned null)';
|
|
151
173
|
if (opts.strict) failed.push({ input, error: reason });
|
|
@@ -190,12 +212,12 @@ async function build(opts) {
|
|
|
190
212
|
|
|
191
213
|
if (opts.check) {
|
|
192
214
|
const staleCount = inputs.length - cached.length;
|
|
193
|
-
return { compiled: [], cached, skipped, failed, staleCount };
|
|
215
|
+
return { compiled: [], cached, skipped, failed, warnings, staleCount };
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
writeCache(opts.cacheFile, newCache);
|
|
197
219
|
|
|
198
|
-
return { compiled, cached, skipped, failed };
|
|
220
|
+
return { compiled, cached, skipped, failed, warnings };
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
async function watch(opts, onReport) {
|