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 CHANGED
@@ -4,6 +4,7 @@ JSON Schema validation that compiles for speed and still runs where code generat
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/ata-validator)](https://www.npmjs.com/package/ata-validator)
6
6
  [![License](https://img.shields.io/npm/l/ata-validator)](LICENSE)
7
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ata-core/ata-validator/badge)](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. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
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]: value does not match format "email"
191
- // --> schemas/user.json:5:7
191
+ // error[ATA3001]: not a valid email: "not-an-email"
192
+ // --> schemas/user.json:5:44
192
193
  // |
193
- // 5 | "email": { "type": "string", "format": "email" }
194
- // | ^^^^^^^ expected format 'email'
194
+ // 5 | "email": { "type": "string", "format": "email" },
195
+ // | ^ expected format 'email'
195
196
  // |
196
- // --> input, byte 23
197
+ // --> input:1:24 (body.email)
197
198
  // |
198
- // 1 | {"name":"M","email":"not-an-email","age":-3}
199
- // | ^^^^^^^^^^^^^^ got "not-an-email"
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`) are native-only and unaffected.
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
- const inner = this.validate;
250
- const root = this._schemaObj;
251
- const { resolvePointer } = require('./lib/pointer.js');
252
- this.validate = (data) => {
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 SAFE_REGEX_SOURCE = require('./safe-regex-source');
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, inlined verbatim into standalone output so a
35
- // compiled module that uses safe `pattern` matchers has no runtime dependency
36
- // on ata-validator. The engine source is baked into `lib/safe-regex-source.js`
37
- // at build time (see `scripts/regen-safe-regex-source.js`), so this path has
38
- // no `fs`/`path`/`__dirname` reads — safe in browser bundles too. The embed
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 body = SAFE_REGEX_SOURCE
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.
@@ -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 parse and call
126
- // validate(). Installed after compilation, so `validator.validate` is final.
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'));
@@ -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
- if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors;
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];