ata-validator 1.23.0 → 1.25.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 +29 -0
- package/bin/ata.js +34 -2
- package/build.d.ts +42 -5
- package/compat.d.ts +8 -0
- package/compat.js +20 -0
- package/index.d.ts +12 -1
- package/index.js +96 -0
- package/lib/aot-build.js +37 -2
- package/lib/aot-impl.js +158 -40
- package/lib/data-positions.js +6 -3
- package/lib/js-compiler.js +203 -8
- package/lib/scan-compiler.js +1007 -0
- package/lib/scan-runtime.js +158 -0
- package/lib/schema-hash.js +39 -0
- package/lib/strict-check.js +53 -0
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
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.25.0 - 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Emitted standalone modules compiled their regex patterns on every call. The compiler stores each pattern both as a closure variable and as an inline `const _reN = __ataSafeRe(...)` in the stored source; in process the inline line never runs (the closure binds the compiled matcher once), but every emitter placed it inside the emitted function, where it shadowed the module-scope binding and rebuilt the matcher per call. A schema with one `pattern` validated at 202 thousand ops/s as a standalone module while the in-process path ran at millions. Patterns are now declared once at load scope in all four emitters (`toStandalone`, `toStandaloneModule`, `bundleStandalone`, `bundleCompact`); the same document on the same schema now measures 27.0 million ops/s, 134x. `bundleCompact` needed care: its deduplication keyed on body text alone, and with the pattern no longer in the body, two schemas differing only in their pattern would have merged, so the key now includes the factory-scope declarations.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Provably local `unevaluated*` compiles real error detail on the generated-code path, and therefore in standalone modules. When every occurrence of `unevaluatedProperties`/`unevaluatedItems` sits on a node with no `$ref` and no `patternProperties`, and every in-place applicator (`allOf`, `anyOf`, `oneOf`, `if`/`then`/`else`) contributes only property names the node's own `properties` already declares, the keyword is exactly `additionalProperties`/`items` in disguise and both the error and combined generators now emit it as such, keeping the keyword's own identity: one error per stray key with `params.unevaluatedProperty` (ATA7003), one error with `params.limit` for extra items. `toStandaloneModule` with error detail requested no longer degrades on these schemas; a config schema of the common `if/then/else` plus root `unevaluatedProperties: false` shape ships an AOT module with the same per-field errors the runtime reports. Schemas where the annotations cannot be proven local (`unevaluated*` next to a `$ref`, cousin-visible shapes) still decline, loudly, as before. Verified by the error-shape differential (2,816 cases), the engine differential (18,126 cases) and the official suite on all three dialects.
|
|
14
|
+
- `toStandaloneModule(schema, { positions: true })` also exports `validateJSON(text)`: parse, validate, and on failure attach a `dataFrame` (`byteOffset`, `length`, `line`, `col`, `text`) to every error by walking the original text once. The mapping goes through `instancePath`, so a key that appears in several sections frames each error on its own occurrence, which a first-occurrence string search does not. A syntax error is the single ATA9001 error with a frame on the document. The walker is the runtime's own `buildDataPositionMap`, embedded verbatim so the two cannot drift, and it costs about 5 KB in the module (1.7 KB gzipped), which is why it is opt-in.
|
|
15
|
+
- `toStandaloneModule(schema, { parse: true })`'s `parse(data)` now fills declared `default` values for absent optional properties (object and array defaults are fresh per call) and is emitted for composed schemas under the same proof the `unevaluated*` error generators use: in-place applicators are admitted when every property name they mention is already declared in the node's own `properties`. The common config shape, `if`/`then`/`else` over declared fields with root `unevaluatedProperties: false`, now gets a `parse` that enforces the conditional, rejects undeclared keys, and returns the ready object. Two default shapes decline because they are where `parse` and the runtime would disagree: a required property with a default, and a default the property's own schema rejects.
|
|
16
|
+
- Every emitted module exports `schemaHash`, a 16 character content hash of the schema it was compiled from, over canonical JSON so key order does not matter, computed from the schema as the caller wrote it so dialect normalization does not break the comparison. `schemaHash(schema)` from `ata-validator/build` computes the same value; a build detects a stale artifact by comparing the two instead of embedding its own fingerprint.
|
|
17
|
+
- The agreement between compiled modules and the runtime is now a stated contract, in `docs/STABILITY.md` and the API reference: same version, same verdict on every document, enforced by the differential test in every CI run, with the emitters declining or degrading loudly where they cannot prove equivalence.
|
|
18
|
+
- The compat shim exports `attachDataFrames(errors, text)`: the same one-pass mapping for Ajv-shaped errors, for callers that keep the source text around. Not an Ajv API; it exists because hand-written line finders based on a string search get repeated keys wrong.
|
|
19
|
+
|
|
20
|
+
## 1.24.0 - 2026-09-16
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- 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.
|
|
27
|
+
- `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.
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- 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.
|
|
33
|
+
|
|
5
34
|
## 1.23.0 - 2026-09-16
|
|
6
35
|
|
|
7
36
|
### Added
|
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; }
|
|
@@ -156,7 +161,13 @@ function cmdCompile(args) {
|
|
|
156
161
|
process.exit(1);
|
|
157
162
|
}
|
|
158
163
|
const input = args._[0];
|
|
159
|
-
|
|
164
|
+
// With no --format, the output filename already says what the caller wants:
|
|
165
|
+
// -o validator.cjs used to emit an ES module into a .cjs file, which Node
|
|
166
|
+
// then refused to load.
|
|
167
|
+
const extFormat = args.opts.output && /\.cjs$/.test(args.opts.output) ? 'cjs'
|
|
168
|
+
: args.opts.output && /\.mjs$/.test(args.opts.output) ? 'esm'
|
|
169
|
+
: null;
|
|
170
|
+
const format = args.opts.format || extFormat || 'esm';
|
|
160
171
|
if (format !== 'esm' && format !== 'cjs') {
|
|
161
172
|
process.stderr.write(`error: --format must be esm or cjs (got "${format}")\n`);
|
|
162
173
|
process.exit(1);
|
|
@@ -180,6 +191,19 @@ function cmdCompile(args) {
|
|
|
180
191
|
process.exit(1);
|
|
181
192
|
}
|
|
182
193
|
|
|
194
|
+
// The build is where an authoring mistake is cheapest to stop: a typo like
|
|
195
|
+
// maxLenght compiles into a module that simply lacks the rule, and nobody
|
|
196
|
+
// revisits a compiled module. Findings fail the compile, with the path and
|
|
197
|
+
// the suggested spelling.
|
|
198
|
+
if (args.opts.strictSchema) {
|
|
199
|
+
const { checkSchemaStrict } = require('../lib/strict-check');
|
|
200
|
+
const problems = checkSchemaStrict(schema, {});
|
|
201
|
+
if (problems.length > 0) {
|
|
202
|
+
for (const x of problems) reportCompileError(input, `strict mode: ${x.message} at ${x.path}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
183
207
|
const { Validator } = require('..');
|
|
184
208
|
const aot = require('../lib/aot');
|
|
185
209
|
let v;
|
|
@@ -200,7 +224,15 @@ function cmdCompile(args) {
|
|
|
200
224
|
}
|
|
201
225
|
}
|
|
202
226
|
const schemaFile = path.relative(process.cwd(), input) || input;
|
|
203
|
-
const
|
|
227
|
+
const compileWarnings = [];
|
|
228
|
+
const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile, onWarning: (w) => compileWarnings.push(w) });
|
|
229
|
+
if (compileWarnings.length > 0) {
|
|
230
|
+
if (args.opts.strict) {
|
|
231
|
+
for (const w of compileWarnings) reportCompileError(input, w);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${w}\n`);
|
|
235
|
+
}
|
|
204
236
|
if (!src) {
|
|
205
237
|
reportCompileError(input, 'schema is too complex for standalone compilation');
|
|
206
238
|
process.exit(1);
|
package/build.d.ts
CHANGED
|
@@ -89,6 +89,18 @@ export interface BundleStandaloneOptions {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export interface ToStandaloneModuleOptions {
|
|
92
|
+
/**
|
|
93
|
+
* Run the authoring-time schema checks before emitting: unknown keywords
|
|
94
|
+
* (with a spelling suggestion), inert keywords, unsatisfiable required
|
|
95
|
+
* names, unresolvable local $refs. Throws with every finding.
|
|
96
|
+
*/
|
|
97
|
+
strictSchema?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Called when the module ships degraded: error detail was requested but the
|
|
100
|
+
* error generator declined this schema, so failures report the single
|
|
101
|
+
* ATA9000 abort-early error while the verdict stays exact.
|
|
102
|
+
*/
|
|
103
|
+
onWarning?: (message: string) => void;
|
|
92
104
|
format?: 'cjs' | 'esm';
|
|
93
105
|
abortEarly?: boolean;
|
|
94
106
|
source?: boolean;
|
|
@@ -99,12 +111,27 @@ export interface ToStandaloneModuleOptions {
|
|
|
99
111
|
formatMode?: 'embed' | 'inject';
|
|
100
112
|
/**
|
|
101
113
|
* Also export `parse(data)`: validate, then return a copy of the input
|
|
102
|
-
* holding only the properties the schema declares
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
114
|
+
* holding only the properties the schema declares, with declared `default`
|
|
115
|
+
* values filled in for absent optional properties (object and array
|
|
116
|
+
* defaults are fresh per call). Off by default because it adds to the
|
|
117
|
+
* emitted module's size. Emitted only where the copy is provably exact:
|
|
118
|
+
* `$ref` and `patternProperties` decline; in-place applicators (`allOf`,
|
|
119
|
+
* `anyOf`, `oneOf`, `if`/`then`/`else`, and `unevaluatedProperties: false`)
|
|
120
|
+
* are admitted when every property name they mention is already declared
|
|
121
|
+
* in the node's own `properties`. A required property with a default, or
|
|
122
|
+
* a default its own schema rejects, also declines, because those are the
|
|
123
|
+
* two shapes where parse() and the runtime would disagree.
|
|
106
124
|
*/
|
|
107
125
|
parse?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Also export `validateJSON(text)`: parse the JSON text, validate, and on
|
|
128
|
+
* failure attach a `dataFrame` ({ byteOffset, length, line, col, text })
|
|
129
|
+
* to every error by walking the original text once, so errors point at the
|
|
130
|
+
* right occurrence even when the same key appears in several sections.
|
|
131
|
+
* Off by default because the embedded position walker adds to the emitted
|
|
132
|
+
* module's size.
|
|
133
|
+
*/
|
|
134
|
+
positions?: boolean;
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
/** Bundle multiple schemas into one self-contained module (no ata-validator runtime). */
|
|
@@ -113,6 +140,16 @@ export function bundleStandalone(schemas: unknown[], options?: BundleStandaloneO
|
|
|
113
140
|
/** Like {@link bundleStandalone} but deduplicates shared bodies for smaller output. */
|
|
114
141
|
export function bundleCompact(schemas: unknown[], options?: BundleStandaloneOptions): string;
|
|
115
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Stable content hash of a schema, 16 hex characters, over a canonical JSON
|
|
145
|
+
* form (keys sorted at every level). Every module from
|
|
146
|
+
* {@link toStandaloneModule} exports its own `schemaHash`; comparing that
|
|
147
|
+
* against `schemaHash(currentSchema)` tells a build the module is stale.
|
|
148
|
+
* An integrity aid, not a security boundary.
|
|
149
|
+
*/
|
|
150
|
+
export function schemaHash(schema: unknown): string;
|
|
151
|
+
|
|
116
152
|
/** Emit a self-contained `validate`/`isValid` module string for a single
|
|
117
|
-
* schema, plus `parse` when {@link ToStandaloneModuleOptions.parse} is set
|
|
153
|
+
* schema, plus `parse` when {@link ToStandaloneModuleOptions.parse} is set
|
|
154
|
+
* and `validateJSON` when {@link ToStandaloneModuleOptions.positions} is. */
|
|
118
155
|
export function toStandaloneModule(schema: unknown, options?: ToStandaloneModuleOptions): string | null;
|
package/compat.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ declare class Ata {
|
|
|
25
25
|
removeKeyword(keyword: string): this;
|
|
26
26
|
|
|
27
27
|
errorsText(errors?: Ata.ErrorObject[] | null, options?: { separator?: string; dataVar?: string }): string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attach a `dataFrame` ({ byteOffset, length, line, col, text }) to each
|
|
31
|
+
* error by mapping its `instancePath` into the JSON text the data was
|
|
32
|
+
* parsed from. One walk of the text; correct when the same key appears in
|
|
33
|
+
* several sections. Not an Ajv API. Mutates and returns `errors`.
|
|
34
|
+
*/
|
|
35
|
+
static attachDataFrames(errors: Ata.ErrorObject[] | null | undefined, text: string | Buffer): Ata.ErrorObject[] | null | undefined;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
declare namespace Ata {
|
package/compat.js
CHANGED
|
@@ -409,6 +409,26 @@ function metaValidator(id) {
|
|
|
409
409
|
return v;
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
// Maps each error's instancePath to its position in the JSON text the data
|
|
413
|
+
// was parsed from and attaches it as `dataFrame` ({ byteOffset, length,
|
|
414
|
+
// line, col, text }). One walk of the text, and correct when the same key
|
|
415
|
+
// appears in several sections, which a first-occurrence string search is
|
|
416
|
+
// not. Not an Ajv API: exported for callers that keep the source text
|
|
417
|
+
// around and want file positions on Ajv-shaped errors.
|
|
418
|
+
Ata.attachDataFrames = function attachDataFrames(errors, text) {
|
|
419
|
+
if (!errors || !errors.length || text == null) return errors;
|
|
420
|
+
const { buildDataPositionMap } = require('./lib/data-positions');
|
|
421
|
+
let map;
|
|
422
|
+
try { map = buildDataPositionMap(text); } catch { return errors; }
|
|
423
|
+
for (const e of errors) {
|
|
424
|
+
if (!e || e.dataFrame) continue;
|
|
425
|
+
const ptr = e.instancePath != null ? e.instancePath : (e.dataPath || '');
|
|
426
|
+
const p = map[ptr];
|
|
427
|
+
if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
|
|
428
|
+
}
|
|
429
|
+
return errors;
|
|
430
|
+
};
|
|
431
|
+
|
|
412
432
|
module.exports = Ata;
|
|
413
433
|
module.exports.default = Ata;
|
|
414
434
|
module.exports.Ata = Ata;
|
package/index.d.ts
CHANGED
|
@@ -395,9 +395,20 @@ export interface ValidatorOptions {
|
|
|
395
395
|
keywords?: Record<string, KeywordDefinition | KeywordValidate>;
|
|
396
396
|
/**
|
|
397
397
|
* When true, validation errors include `parentSchema` (the schema object
|
|
398
|
-
* that produced the error)
|
|
398
|
+
* that produced the error), `schema` (the failing keyword's own value) and
|
|
399
|
+
* `data` (the value the error points at). Matches ajv's `verbose: true`.
|
|
399
400
|
*/
|
|
400
401
|
verbose?: boolean;
|
|
402
|
+
/**
|
|
403
|
+
* Authoring-time schema checks, run at construction on the schema as
|
|
404
|
+
* written: unknown keywords (with a spelling suggestion), keywords the
|
|
405
|
+
* node's own `type` makes inert, `required` names nothing can satisfy, and
|
|
406
|
+
* local `$ref`s that do not resolve. `true` throws with every finding;
|
|
407
|
+
* `'log'` warns through `logger` (or the console) and continues.
|
|
408
|
+
*/
|
|
409
|
+
strictSchema?: boolean | 'log';
|
|
410
|
+
/** Receives `strictSchema: 'log'` warnings; `false` silences them. */
|
|
411
|
+
logger?: { warn(...args: unknown[]): void } | false;
|
|
401
412
|
/**
|
|
402
413
|
* When true, validate() returns a shared frozen result on the first failure
|
|
403
414
|
* instead of collecting full error details. Smaller hot-path allocation.
|
package/index.js
CHANGED
|
@@ -933,6 +933,8 @@ class Validator {
|
|
|
933
933
|
// to attach dataFrame entries to each enriched error.
|
|
934
934
|
this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
|
|
935
935
|
this._lastRawInput = null;
|
|
936
|
+
// undefined: not built yet. null: this schema has no scanner.
|
|
937
|
+
this._scanner = undefined;
|
|
936
938
|
|
|
937
939
|
// Public methods start as memoized accessors on the prototype; nothing is
|
|
938
940
|
// allocated per instance until one is first read. See _defineLazyMethod
|
|
@@ -1426,6 +1428,23 @@ class Validator {
|
|
|
1426
1428
|
return verdictFromText(jsonStr);
|
|
1427
1429
|
}
|
|
1428
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.
|
|
1429
1448
|
// validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
|
|
1430
1449
|
// validate) so it works with or without the native addon and in browsers.
|
|
1431
1450
|
{
|
|
@@ -1799,6 +1818,83 @@ class Validator {
|
|
|
1799
1818
|
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
|
|
1800
1819
|
if (bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords)) installSlowBufferApis(this);
|
|
1801
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
|
+
|
|
1802
1898
|
|
|
1803
1899
|
// Save to identity cache for ultra-fast reuse with same schema object.
|
|
1804
1900
|
// Only an instance built without options may answer a later
|
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) {
|
|
@@ -248,10 +270,22 @@ function bundleCompact(schemas, opts) {
|
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
function toStandaloneModule(schema, opts) {
|
|
273
|
+
// The same authoring-time checks `ata compile --strict-schema` runs, for
|
|
274
|
+
// callers driving the build API directly: a typo in a schema headed for a
|
|
275
|
+
// compiled module is the quietest place a typo can live.
|
|
276
|
+
if (opts && opts.strictSchema) {
|
|
277
|
+
const { checkSchemaStrict } = require('./strict-check');
|
|
278
|
+
const problems = checkSchemaStrict(schema, {});
|
|
279
|
+
if (problems.length > 0) {
|
|
280
|
+
throw new Error(problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n'));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
251
283
|
const v = schema instanceof Validator ? schema : new Validator(schema, opts);
|
|
252
284
|
return aot.toStandaloneModule(v, opts);
|
|
253
285
|
}
|
|
254
286
|
|
|
287
|
+
const { schemaHash } = require('./schema-hash');
|
|
288
|
+
|
|
255
289
|
module.exports = {
|
|
256
290
|
build,
|
|
257
291
|
expandGlobs,
|
|
@@ -261,4 +295,5 @@ module.exports = {
|
|
|
261
295
|
bundleStandalone,
|
|
262
296
|
bundleCompact,
|
|
263
297
|
toStandaloneModule,
|
|
298
|
+
schemaHash,
|
|
264
299
|
};
|