ata-validator 1.38.0 → 1.39.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/index.d.ts +2 -1
- package/index.js +12 -28
- package/lib/aot-impl.js +40 -15
- package/lib/buffer-gate.js +6 -7
- package/lib/enrich-error.js +4 -2
- package/lib/js-compiler.js +329 -46
- package/lib/rejections.js +84 -39
- package/lib/render-pretty.js +15 -11
- package/lib/safe-regex.js +424 -413
- package/lib/schema-order.js +18 -2
- package/lib/validator-core.js +99 -13
- package/lib/version.js +1 -1
- package/package.json +11 -11
- package/lib/safe-regex-source.js +0 -8
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ JSON Schema validation that compiles for speed and still runs where code generat
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/ata-validator)
|
|
6
6
|
[](LICENSE)
|
|
7
|
+
[](https://scorecard.dev/viewer/?uri=github.com/ata-core/ata-validator)
|
|
7
8
|
|
|
8
9
|
1.0 is a stability commitment: see [docs/STABILITY.md](docs/STABILITY.md) for the semver, deprecation, and error-code guarantees.
|
|
9
10
|
|
|
@@ -32,13 +33,13 @@ npm install --save-dev ata-validator
|
|
|
32
33
|
npx ata build 'schemas/*.json' --out-dir src/generated
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
|
|
36
|
+
The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc (2.17 or later, below the 2.28 Node itself needs) and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
|
|
36
37
|
|
|
37
38
|
```bash
|
|
38
39
|
npm install ata-validator --omit=optional
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases.
|
|
42
|
+
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
|
|
42
43
|
|
|
43
44
|
Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
|
|
44
45
|
|
|
@@ -187,16 +188,16 @@ const v = new Validator(schema, { source: { path: 'schemas/user.json', content:
|
|
|
187
188
|
const r = v.validateJSON(input)
|
|
188
189
|
if (!r.valid) {
|
|
189
190
|
console.error(renderPretty(r.errors))
|
|
190
|
-
// error[ATA3001]:
|
|
191
|
-
// --> schemas/user.json:5:
|
|
191
|
+
// error[ATA3001]: not a valid email: "not-an-email"
|
|
192
|
+
// --> schemas/user.json:5:44
|
|
192
193
|
// |
|
|
193
|
-
// 5 |
|
|
194
|
-
// |
|
|
194
|
+
// 5 | "email": { "type": "string", "format": "email" },
|
|
195
|
+
// | ^ expected format 'email'
|
|
195
196
|
// |
|
|
196
|
-
// --> input
|
|
197
|
+
// --> input:1:24 (body.email)
|
|
197
198
|
// |
|
|
198
|
-
// 1 | {"name":"
|
|
199
|
-
// |
|
|
199
|
+
// 1 | {"name":"Mert","email":"not-an-email","age":26}
|
|
200
|
+
// | ^^^^^^^^^^^^^^ found "not-an-email"
|
|
200
201
|
// |
|
|
201
202
|
// = help: missing '@' and domain part
|
|
202
203
|
// = note: see https://ata-validator.com/e/ATA3001
|
package/index.d.ts
CHANGED
|
@@ -439,7 +439,8 @@ export interface ValidatorOptions {
|
|
|
439
439
|
* answers `validate()`, `isValidObject()` and `validateJSON()`. For a schema
|
|
440
440
|
* that arrives from outside the trust boundary. The verdict is the same on
|
|
441
441
|
* every engine; the cost is not. The buffer APIs (`isValid`, `countValid`,
|
|
442
|
-
* `batchIsValid`)
|
|
442
|
+
* `batchIsValid`) use the native addon where it loads, which this option
|
|
443
|
+
* does not affect; without it they answer through the engine it picks.
|
|
443
444
|
*/
|
|
444
445
|
engine?: 'auto' | 'interpreter';
|
|
445
446
|
}
|
package/index.js
CHANGED
|
@@ -36,7 +36,7 @@ class TextRejection {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function installCodegenPaths (ctx) {
|
|
39
|
-
const { ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindVerdict, _jsonSyntaxRejection, _mustReject, getNative, isV1Dialect, resolveSchemaByPath } = core._internals;
|
|
39
|
+
const { ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindVerdict, _jsonSyntaxRejection, _mustReject, _verboseWrap, getNative, isV1Dialect, resolveSchemaByPath } = core._internals;
|
|
40
40
|
const { jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr } = ctx;
|
|
41
41
|
// errFn: the generated error function when it is safe, else the
|
|
42
42
|
// interpreted engine, on every platform alike.
|
|
@@ -222,6 +222,11 @@ function installCodegenPaths (ctx) {
|
|
|
222
222
|
this.validate = preprocess
|
|
223
223
|
? (data) => { preprocess(data); return run(data); }
|
|
224
224
|
: run;
|
|
225
|
+
// What validate() returns for a document already known to fail, without
|
|
226
|
+
// deciding again: the lazy layer in lib/validator-core.js has its verdict
|
|
227
|
+
// and used to call validate() to get the errors, which ran the verdict a
|
|
228
|
+
// second time and built a second rejection around it.
|
|
229
|
+
if (!preprocess) ctx.rejectBase = onReject;
|
|
225
230
|
} else {
|
|
226
231
|
// No hybrid factory, so the assembly needs the function itself rather
|
|
227
232
|
// than a reference it can call later: build it now.
|
|
@@ -230,6 +235,7 @@ function installCodegenPaths (ctx) {
|
|
|
230
235
|
this.validate = preprocess
|
|
231
236
|
? (data) => { preprocess(data); return safeCombinedFn(data); }
|
|
232
237
|
: safeCombinedFn;
|
|
238
|
+
if (!preprocess) ctx.rejectBase = (data) => _mustReject(safeCombinedFn(data));
|
|
233
239
|
} else {
|
|
234
240
|
this.validate = preprocess
|
|
235
241
|
? (data) => {
|
|
@@ -237,6 +243,7 @@ function installCodegenPaths (ctx) {
|
|
|
237
243
|
return jsFn(data) ? VALID_RESULT : errOnly(data);
|
|
238
244
|
}
|
|
239
245
|
: (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
|
|
246
|
+
if (!preprocess) ctx.rejectBase = errOnly;
|
|
240
247
|
}
|
|
241
248
|
}
|
|
242
249
|
// Verbose mode: populate parentSchema, schema and data on each error, the
|
|
@@ -246,33 +253,10 @@ function installCodegenPaths (ctx) {
|
|
|
246
253
|
// migration ended up writing by hand. Errors may be frozen, so clone
|
|
247
254
|
// them with the extra fields.
|
|
248
255
|
if (this._verbose) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.validate = (
|
|
253
|
-
const result = inner(data);
|
|
254
|
-
if (result && !result.valid && result.errors) {
|
|
255
|
-
const enriched = result.errors.map((err) => {
|
|
256
|
-
if (!err || err.parentSchema !== undefined) return err;
|
|
257
|
-
const parentSchema = resolveSchemaByPath(root, err.schemaPath);
|
|
258
|
-
// The last segment of the schema path is the keyword that
|
|
259
|
-
// failed, so its value on the parent is that keyword's schema.
|
|
260
|
-
const sp = typeof err.schemaPath === 'string' ? err.schemaPath : '';
|
|
261
|
-
const last = sp.slice(sp.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
262
|
-
const keywordSchema = (parentSchema !== null && typeof parentSchema === 'object' && last)
|
|
263
|
-
? parentSchema[last]
|
|
264
|
-
: undefined;
|
|
265
|
-
return {
|
|
266
|
-
...err,
|
|
267
|
-
parentSchema,
|
|
268
|
-
schema: keywordSchema,
|
|
269
|
-
data: resolvePointer(data, err.instancePath, undefined),
|
|
270
|
-
};
|
|
271
|
-
});
|
|
272
|
-
return { valid: false, errors: enriched };
|
|
273
|
-
}
|
|
274
|
-
return result;
|
|
275
|
-
};
|
|
256
|
+
// The verbose fields are added here, so a path around this layer would
|
|
257
|
+
// miss them.
|
|
258
|
+
ctx.rejectBase = null;
|
|
259
|
+
this.validate = _verboseWrap(this.validate, this._schemaObj);
|
|
276
260
|
}
|
|
277
261
|
// The verdict methods answer validate()'s question without building the
|
|
278
262
|
// error list, so they run the same preprocess pass. Skipping it made the
|
package/lib/aot-impl.js
CHANGED
|
@@ -17,7 +17,7 @@ const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions }
|
|
|
17
17
|
const { buildTargetedPositionMap } = require('./data-positions');
|
|
18
18
|
const { schemaHash } = require('./schema-hash');
|
|
19
19
|
const ATA_VERSION = require('./version');
|
|
20
|
-
const
|
|
20
|
+
const { engineSource: safeRegexEngineSource } = require('./safe-regex');
|
|
21
21
|
|
|
22
22
|
// Embedded verbatim in standalone modules so the output file has no runtime
|
|
23
23
|
// dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
|
|
@@ -31,21 +31,16 @@ const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
|
31
31
|
return len;
|
|
32
32
|
}`;
|
|
33
33
|
|
|
34
|
-
// The linear-time regex engine,
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
|
|
40
|
-
// alias the emitted code calls. The engine has no eval/new Function, so the
|
|
34
|
+
// The linear-time regex engine, embedded in standalone modules whose patterns
|
|
35
|
+
// need it so the output does not depend on ata-validator at run time. The text
|
|
36
|
+
// is the engine's own function (lib/safe-regex.js), called once for the
|
|
37
|
+
// `__ataSafeRe` the emitted code uses. No fs, path or __dirname, so this is
|
|
38
|
+
// safe in browser bundles, and the engine has no eval or new Function, so the
|
|
41
39
|
// embed is CSP-safe.
|
|
42
40
|
let _safeRegexEmbed = null;
|
|
43
41
|
function getSafeRegexEmbed() {
|
|
44
42
|
if (_safeRegexEmbed === null) {
|
|
45
|
-
const
|
|
46
|
-
.replace(/^'use strict'\s*\n/, '')
|
|
47
|
-
.replace(/\nmodule\.exports[^\n]*\n?/, '\n');
|
|
48
|
-
_safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
|
|
43
|
+
_safeRegexEmbed = 'const __ataSafeRe = (' + safeRegexEngineSource() + ')().compileSafe;';
|
|
49
44
|
}
|
|
50
45
|
return _safeRegexEmbed;
|
|
51
46
|
}
|
|
@@ -191,6 +186,24 @@ function emitFormatDecls(closures, mode, declKW) {
|
|
|
191
186
|
|
|
192
187
|
const { emitClone, inlineRefsForClone } = require('./clone-emit');
|
|
193
188
|
|
|
189
|
+
// The schema-source frames the error function names, as `__ataSS[i]`. Each
|
|
190
|
+
// is built once, frozen, from a [line, col, text index] row, with every source
|
|
191
|
+
// line's text written once. Frozen, so sharing one object between reads is safe.
|
|
192
|
+
function sourceFrameDecls(frames, schemaFile) {
|
|
193
|
+
if (!frames || frames.size === 0) return '';
|
|
194
|
+
const texts = [];
|
|
195
|
+
const textIndex = new Map();
|
|
196
|
+
const rows = [];
|
|
197
|
+
for (const f of frames.values()) {
|
|
198
|
+
let i = textIndex.get(f.text);
|
|
199
|
+
if (i === undefined) { i = texts.length; texts.push(f.text); textIndex.set(f.text, i); }
|
|
200
|
+
rows[f.index] = `[${f.line},${f.col},${i}]`;
|
|
201
|
+
}
|
|
202
|
+
return `const __ataSF = ${JSON.stringify(schemaFile)};\n` +
|
|
203
|
+
`const __ataSL = ${JSON.stringify(texts)};\n` +
|
|
204
|
+
`const __ataSS = [${rows.join(',')}].map((r) => Object.freeze({ file: __ataSF, line: r[0], col: r[1], text: __ataSL[r[2]] }));\n`;
|
|
205
|
+
}
|
|
206
|
+
|
|
194
207
|
function toStandaloneModule(validator, opts) {
|
|
195
208
|
assertEmittable(validator, 'toStandaloneModule');
|
|
196
209
|
validator._ensureCompiled();
|
|
@@ -205,15 +218,27 @@ function toStandaloneModule(validator, opts) {
|
|
|
205
218
|
|
|
206
219
|
let errCore = '';
|
|
207
220
|
let jsErrFn = null;
|
|
221
|
+
const sourceFrames = new Map();
|
|
208
222
|
if (!abortEarly) {
|
|
209
223
|
jsErrFn = compileToJSCodegenWithErrors(
|
|
210
224
|
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
211
225
|
null,
|
|
212
226
|
validator._userFormats,
|
|
213
|
-
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
|
|
227
|
+
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames } : null,
|
|
214
228
|
);
|
|
215
229
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
216
|
-
if (errSrc) {
|
|
230
|
+
if (errSrc && jsErrFn._errFactory) {
|
|
231
|
+
// The helpers (patterns, name sets, definition and branch functions) are
|
|
232
|
+
// built once, not on every call. With $defs the
|
|
233
|
+
// cycle guard's state sits beside them, so a call made while one is
|
|
234
|
+
// running takes a function from a fresh factory call.
|
|
235
|
+
// Built on the first error read rather than at import, so a page that
|
|
236
|
+
// only ever accepts pays nothing for them at load.
|
|
237
|
+
const fac = jsErrFn._errFactory;
|
|
238
|
+
errCore = jsErrFn._errGuarded
|
|
239
|
+
? `const _mkErr = function() {\n ${fac}\n};\nlet _errMain = null;\nlet _errBusy = false;\nconst errFn = function(d, _all) { if (_errBusy) return _mkErr()(d, _all); if (_errMain === null) _errMain = _mkErr(); _errBusy = true; try { return _errMain(d, _all); } finally { _errBusy = false; } };\n`
|
|
240
|
+
: `const _mkErr = function() {\n ${fac}\n};\nlet _errMain = null;\nconst errFn = function(d, _all) { return (_errMain || (_errMain = _mkErr()))(d, _all); };\n`;
|
|
241
|
+
} else if (errSrc) {
|
|
217
242
|
errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
|
|
218
243
|
} else if (opts && typeof opts.onWarning === 'function') {
|
|
219
244
|
// The caller asked for error detail and is not getting it: the error
|
|
@@ -239,7 +264,7 @@ function toStandaloneModule(validator, opts) {
|
|
|
239
264
|
// entirely so size budgets and grep-based "is this source-mapped?" checks
|
|
240
265
|
// both work.
|
|
241
266
|
const schemaSourceConst = (source && schemaFile)
|
|
242
|
-
? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
|
|
267
|
+
? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n` + sourceFrameDecls(sourceFrames, schemaFile)
|
|
243
268
|
: '';
|
|
244
269
|
|
|
245
270
|
// Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
|
package/lib/buffer-gate.js
CHANGED
|
@@ -122,14 +122,13 @@ function toText(input, name) {
|
|
|
122
122
|
throw new TypeError(`${name}() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().`);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// Replaces the instance's buffer APIs with versions that
|
|
126
|
-
// validate()
|
|
125
|
+
// Replaces the instance's buffer APIs with versions that decode the bytes and
|
|
126
|
+
// ask isValidJSON(), which answers as validate() does on the parsed value and
|
|
127
|
+
// reads text the scanner covers without parsing it (a small document went from
|
|
128
|
+
// 350 to 155 ns without the addon). Its own native route is behind this same
|
|
129
|
+
// gate, so a schema routed here never reaches the walker through it.
|
|
127
130
|
function installSlowBufferApis(validator) {
|
|
128
|
-
const isValidText = (text) =>
|
|
129
|
-
let value;
|
|
130
|
-
try { value = JSON.parse(text); } catch { return false; }
|
|
131
|
-
return validator.validate(value).valid;
|
|
132
|
-
};
|
|
131
|
+
const isValidText = (text) => validator.isValidJSON(text);
|
|
133
132
|
validator.isValid = (input) => isValidText(toText(input, 'isValid'));
|
|
134
133
|
validator.isValidPrepadded = (paddedBuffer, jsonLength) =>
|
|
135
134
|
isValidText(Buffer.from(paddedBuffer.buffer, paddedBuffer.byteOffset, jsonLength).toString('utf8'));
|
package/lib/enrich-error.js
CHANGED
|
@@ -233,8 +233,10 @@ function enrich (rawErr, opts) {
|
|
|
233
233
|
if ('schema' in rawErr) out.schema = rawErr.schema;
|
|
234
234
|
|
|
235
235
|
// oneOf/anyOf collapse: preserve the nested branch errors so the pretty
|
|
236
|
-
// renderer can surface the closest variant's diagnostics.
|
|
237
|
-
|
|
236
|
+
// renderer can surface the closest variant's diagnostics. They are enriched
|
|
237
|
+
// like any other error: passed through raw, they carried whatever the engine
|
|
238
|
+
// that answered wrote, the generator's internal ordering key included.
|
|
239
|
+
if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => enrich(b, data !== undefined ? { data } : null));
|
|
238
240
|
|
|
239
241
|
if (positions && positions[path]) {
|
|
240
242
|
const p = positions[path];
|