ata-validator 1.39.1 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
 
@@ -69,6 +70,8 @@ if (isValid(req.body)) {
69
70
 
70
71
  The `.compiled.mjs` modules are self-contained: zero runtime dependency on ata-validator, fully tree-shakeable, with TypeScript types emitted alongside.
71
72
 
73
+ Code already written with `new Validator(schema)` gets the same result from the bundler plugin, without changes: [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata) replaces each call whose schema is known at build time with a compiled validator that answers the same, and the runtime leaves the bundle. For the plugin's three-schema test entry a minified Vite build goes from 122.5 KB to 16.7 KB gzipped on ata 1.39.2. Schemas that arrive at runtime keep the runtime, which is the right tool for them.
74
+
72
75
  ## Measured by others
73
76
 
74
77
  Public harnesses run ata without ata's involvement. Quote these before anything in this
@@ -187,16 +190,16 @@ const v = new Validator(schema, { source: { path: 'schemas/user.json', content:
187
190
  const r = v.validateJSON(input)
188
191
  if (!r.valid) {
189
192
  console.error(renderPretty(r.errors))
190
- // error[ATA3001]: value does not match format "email"
191
- // --> schemas/user.json:5:7
193
+ // error[ATA3001]: not a valid email: "not-an-email"
194
+ // --> schemas/user.json:5:44
192
195
  // |
193
- // 5 | "email": { "type": "string", "format": "email" }
194
- // | ^^^^^^^ expected format 'email'
196
+ // 5 | "email": { "type": "string", "format": "email" },
197
+ // | ^ expected format 'email'
195
198
  // |
196
- // --> input, byte 23
199
+ // --> input:1:24 (body.email)
197
200
  // |
198
- // 1 | {"name":"M","email":"not-an-email","age":-3}
199
- // | ^^^^^^^^^^^^^^ got "not-an-email"
201
+ // 1 | {"name":"Mert","email":"not-an-email","age":26}
202
+ // | ^^^^^^^^^^^^^^ found "not-an-email"
200
203
  // |
201
204
  // = help: missing '@' and domain part
202
205
  // = note: see https://ata-validator.com/e/ATA3001
@@ -259,7 +262,8 @@ for. Speed is the same either way once warm.
259
262
 
260
263
  **In a browser, on an edge runtime, or anywhere cold starts are charged, compile.** This
261
264
  is where the difference is the whole story, and it is also where `new Function` is often
262
- blocked outright, which the compiled module does not need.
265
+ blocked outright, which the compiled module does not need. With a bundler, adding `@ata-project/unplugin`
266
+ is enough: it compiles `new Validator(schema)` calls at build time, and leaves the rest alone.
263
267
 
264
268
  ## When to use the runtime API instead
265
269
 
@@ -545,10 +549,12 @@ To get the compiled module without changing code written against the runtime API
545
549
  `new Validator(schema)` whose schema is known at build time is replaced with the compiled
546
550
  module wrapped by `fromCompiled()` from `ata-validator/compiled`, which answers `validate()`,
547
551
  `isValidObject()`, `validateJSON()` and `isValidJSON()` as the runtime does, defaults and errors
548
- included. For the plugin's three-schema test entry a minified Vite build goes from 115.7 KB to
549
- 15.5 KB gzipped. In Node, loading the wrapper and a compiled module and answering the first two
552
+ included. For the plugin's three-schema test entry a minified Vite build goes from 122.5 KB to
553
+ 16.7 KB gzipped on ata 1.39.2. In Node, loading the wrapper and a compiled module and answering the first two
550
554
  checks takes 1.12 ms where the runtime takes 6.63 ms (median of 15 fresh processes). Across
551
- SchemaStore's 977 schemas, 725 can be compiled away; the rest stay on the runtime.
555
+ SchemaStore's 977 schemas, 725 can be compiled away; the rest stay on the runtime. Where the code only calls `isValidObject()` or `isValidJSON()`, the plugin uses
556
+ `fromCompiledVerdict()` from `ata-validator/compiled-verdict` instead, which carries no error pipeline: a
557
+ small app that only asks for a boolean bundles to 2.1 KB gzipped, against 11.5 KB with the full wrapper.
552
558
 
553
559
  Programmatic API if you prefer to script it:
554
560
 
@@ -0,0 +1,11 @@
1
+ // ata-validator/compiled-verdict: the wrapper a bundler plugin puts in place of
2
+ // `new Validator(schema)` for a schema known at build time, when the code only
3
+ // asks for a boolean. No error pipeline is included.
4
+ import type { CompiledModule, CompiledOptions } from './compiled.js';
5
+
6
+ export interface CompiledVerdictValidator<T = unknown> {
7
+ isValidObject(data: unknown): data is T;
8
+ isValidJSON(json: string): boolean;
9
+ }
10
+
11
+ export function fromCompiledVerdict<T = unknown>(mod: Pick<CompiledModule, 'isValid'>, schema: object, options?: CompiledOptions): CompiledVerdictValidator<T>;
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/compiled-verdict: the wrapper a bundler plugin puts in place of
4
+ // `new Validator(schema)` when the schema is known at build time and the code
5
+ // only calls isValidObject() or isValidJSON(). It carries no error pipeline;
6
+ // code that reads errors gets ata-validator/compiled instead.
7
+ module.exports = require('./lib/compiled-verdict.js');
@@ -0,0 +1,3 @@
1
+ import mod from './compiled-verdict.js';
2
+ export const { fromCompiledVerdict } = mod;
3
+ export default mod;
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
@@ -186,6 +186,24 @@ function emitFormatDecls(closures, mode, declKW) {
186
186
 
187
187
  const { emitClone, inlineRefsForClone } = require('./clone-emit');
188
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
+
189
207
  function toStandaloneModule(validator, opts) {
190
208
  assertEmittable(validator, 'toStandaloneModule');
191
209
  validator._ensureCompiled();
@@ -200,12 +218,13 @@ function toStandaloneModule(validator, opts) {
200
218
 
201
219
  let errCore = '';
202
220
  let jsErrFn = null;
221
+ const sourceFrames = new Map();
203
222
  if (!abortEarly) {
204
223
  jsErrFn = compileToJSCodegenWithErrors(
205
224
  typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
206
225
  null,
207
226
  validator._userFormats,
208
- (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
227
+ (source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames } : null,
209
228
  );
210
229
  const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
211
230
  if (errSrc && jsErrFn._errFactory) {
@@ -245,7 +264,7 @@ function toStandaloneModule(validator, opts) {
245
264
  // entirely so size budgets and grep-based "is this source-mapped?" checks
246
265
  // both work.
247
266
  const schemaSourceConst = (source && schemaFile)
248
- ? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
267
+ ? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n` + sourceFrameDecls(sourceFrames, schemaFile)
249
268
  : '';
250
269
 
251
270
  // Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ // What the two compiled wrappers share: which Validator options they accept,
4
+ // the defaults pass, and JSON parsing. lib/compiled.js answers all four entry
5
+ // points; lib/compiled-verdict.js answers only the two that return a boolean,
6
+ // so a bundle that never reads errors does not carry the error pipeline.
7
+
8
+ const { buildDefaultsApplier } = require('./defaults');
9
+
10
+ // The Validator options the wrapper answers the same way as the runtime.
11
+ // Anything else changes what the runtime does in a way the wrapper does not
12
+ // reproduce, so it is refused rather than ignored.
13
+ const COMPILED_OPTIONS = Object.freeze(['useDefaults']);
14
+
15
+ // With defaults, the document is filled in before it is checked, on every
16
+ // entry point, as the runtime does.
17
+ function prepare(mod, schema, options, name) {
18
+ if (options !== undefined && (options === null || typeof options !== 'object')) {
19
+ throw new TypeError(`${name} options must be an object`);
20
+ }
21
+ for (const key of Object.keys(options || {})) {
22
+ if (!COMPILED_OPTIONS.includes(key)) {
23
+ throw new TypeError(`${name} does not support the ${key} option; a Validator with it has to stay on the runtime`);
24
+ }
25
+ }
26
+ const fill = options && options.useDefaults === false ? null : buildDefaultsApplier(schema);
27
+ const isValid = fill ? (d) => { fill(d); return mod.isValid(d); } : mod.isValid;
28
+ return { fill, isValid };
29
+ }
30
+
31
+ function parse(text) {
32
+ try { return { value: JSON.parse(text) }; } catch (e) {
33
+ if (!(e instanceof SyntaxError)) throw e;
34
+ return { error: e };
35
+ }
36
+ }
37
+
38
+ // A frozen stand-in, as the runtime reports, when a document fails and neither
39
+ // the schema nor the extension says why (the data changed in between).
40
+ const EXT_FALLBACK = Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' });
41
+
42
+ // A check the schema does not carry, registered the way @ata-project/keywords
43
+ // registers `instanceof` and `typeof` on a Validator: `_extendChecks(resolve)`
44
+ // before the first call, where resolve() returns { check, errors } or null.
45
+ // The wrapper answers as the runtime does under such a check
46
+ // (validator-core.js, _extendValidate and _jsonEntryWrappers): the verdict is
47
+ // the schema's and the check's; validate() lists the schema's errors, then
48
+ // the check's; the JSON entry points run the check only on text the schema
49
+ // accepts. `extend(ext)` returns the wrapper's methods under the check, and is
50
+ // called once, on the first call after registering, with the resolved check.
51
+ function installExtension(api, schema, extend) {
52
+ let resolvers = null;
53
+ const define = (name, value) => Object.defineProperty(api, name, { value, configurable: true, writable: true, enumerable: false });
54
+ define('_schemaObj', schema);
55
+ define('_initialized', false);
56
+ define('_extendChecks', function (resolve) {
57
+ if (typeof resolve !== 'function') throw new TypeError('_extendChecks expects a function');
58
+ if (resolvers === null) {
59
+ resolvers = [];
60
+ const base = {};
61
+ for (const name of Object.keys(api)) base[name] = api[name];
62
+ // Resolved on the first call to any method, as the runtime resolves at
63
+ // its first compile, and the methods are bound once from then on.
64
+ let bound = null;
65
+ const bind = () => {
66
+ if (bound === null) {
67
+ let ext = null;
68
+ for (const r of resolvers) {
69
+ const x = r();
70
+ if (!x) continue;
71
+ ext = ext === null ? x : combine(ext, x);
72
+ }
73
+ bound = ext === null ? base : extend(base, ext);
74
+ for (const name of Object.keys(base)) api[name] = bound[name];
75
+ }
76
+ return bound;
77
+ };
78
+ for (const name of Object.keys(base)) api[name] = function (arg) { return bind()[name](arg); };
79
+ }
80
+ resolvers.push(resolve);
81
+ return api;
82
+ });
83
+ }
84
+
85
+ function combine(a, b) {
86
+ return {
87
+ check: (d) => a.check(d) && b.check(d),
88
+ errors: (d) => {
89
+ const x = a.errors(d), y = b.errors(d);
90
+ if (!x) return y;
91
+ if (!y) return x;
92
+ return x.concat(y);
93
+ },
94
+ };
95
+ }
96
+
97
+ // The JSON entry points under a check, as the runtime has them.
98
+ function extendJSON(base, { check, errors }) {
99
+ return {
100
+ validateJSON: base.validateJSON && ((text) => {
101
+ const res = base.validateJSON(text);
102
+ if (!res.valid) return res;
103
+ const p = parse(text);
104
+ if (p.error) return res;
105
+ if (check(p.value)) return res;
106
+ const e = errors(p.value);
107
+ return e && e.length ? { valid: false, errors: e } : { valid: false, errors: [EXT_FALLBACK] };
108
+ }),
109
+ isValidJSON: (text) => {
110
+ if (!base.isValidJSON(text)) return false;
111
+ const p = parse(text);
112
+ return p.error ? true : check(p.value);
113
+ },
114
+ };
115
+ }
116
+
117
+ module.exports = { COMPILED_OPTIONS, prepare, parse, installExtension, extendJSON, EXT_FALLBACK };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ // The verdict half of lib/compiled.js: isValidObject() and isValidJSON(),
4
+ // answered as a Validator with default options answers them, defaults filled
5
+ // in first. A bundler plugin puts this in place of `new Validator(schema)`
6
+ // when the code only ever asks for a boolean, so the error pipeline (the
7
+ // rejection classes, enrichment, suggestions, JSON positions) stays out of the
8
+ // bundle. Anything that reads errors needs ata-validator/compiled.
9
+
10
+ const { prepare, parse, installExtension, extendJSON } = require('./compiled-common');
11
+
12
+ function fromCompiledVerdict(mod, schema, options) {
13
+ const { isValid } = prepare(mod, schema, options, 'fromCompiledVerdict');
14
+ const api = {
15
+ isValidObject(data) {
16
+ return isValid(data);
17
+ },
18
+ isValidJSON(text) {
19
+ const p = parse(text);
20
+ return p.error ? false : isValid(p.value);
21
+ },
22
+ };
23
+ installExtension(api, schema, (base, ext) => ({
24
+ isValidObject: (d) => base.isValidObject(d) && ext.check(d),
25
+ isValidJSON: extendJSON(base, ext).isValidJSON,
26
+ }));
27
+ return api;
28
+ }
29
+
30
+ module.exports = { fromCompiledVerdict };
package/lib/compiled.js CHANGED
@@ -16,14 +16,10 @@
16
16
  // leaves the input as it is, as the runtime then does.
17
17
 
18
18
  const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy } = require('./rejections');
19
- const { buildDefaultsApplier } = require('./defaults');
19
+ const { COMPILED_OPTIONS, prepare, parse, installExtension, extendJSON, EXT_FALLBACK } = require('./compiled-common');
20
20
 
21
21
  const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
22
22
 
23
- // The Validator options the wrapper answers the same way as the runtime.
24
- // Anything else changes what the runtime does in a way the wrapper does not
25
- // reproduce, so it is refused rather than ignored.
26
- const COMPILED_OPTIONS = Object.freeze(['useDefaults']);
27
23
  const EMPTY_ERRORS = Object.freeze([]);
28
24
  const VERDICT_DISAGREES = Object.freeze({
29
25
  valid: false,
@@ -50,23 +46,12 @@ class CompiledState {
50
46
  }
51
47
 
52
48
  function fromCompiled(mod, schema, options) {
53
- if (options !== undefined && (options === null || typeof options !== 'object')) {
54
- throw new TypeError('fromCompiled options must be an object');
55
- }
56
- for (const key of Object.keys(options || {})) {
57
- if (!COMPILED_OPTIONS.includes(key)) {
58
- throw new TypeError(`fromCompiled does not support the ${key} option; a Validator with it has to stay on the runtime`);
59
- }
60
- }
49
+ const { fill, isValid } = prepare(mod, schema, options, 'fromCompiled');
61
50
  const self = new CompiledState(schema);
62
- const fill = options && options.useDefaults === false ? null : buildDefaultsApplier(schema);
63
51
  if (fill) {
64
52
  self._mutatesInput = true;
65
53
  self._preprocess = fill;
66
54
  }
67
- // With defaults, the document is filled in before it is checked, on every
68
- // entry point, as the runtime does.
69
- const isValid = fill ? (d) => { fill(d); return mod.isValid(d); } : mod.isValid;
70
55
  const errFn = mod.validate;
71
56
  // The generated function's own result, rejected: if it says valid where the
72
57
  // verdict said no, the disagreement is reported rather than turned into an
@@ -78,13 +63,7 @@ function fromCompiled(mod, schema, options) {
78
63
  const rich = (data) => new RichRejection(raw(data), data, null, self, schema, _enrichLazy);
79
64
  const buildErrors = (data) => rich(data).errors;
80
65
  const buildRawErrors = (data) => rich(data)._ataRaw();
81
- const parse = (text) => {
82
- try { return { value: JSON.parse(text) }; } catch (e) {
83
- if (!(e instanceof SyntaxError)) throw e;
84
- return { error: e };
85
- }
86
- };
87
- return {
66
+ const api = {
88
67
  validate(data) {
89
68
  if (isValid(data)) return { valid: true, data, errors: EMPTY_ERRORS };
90
69
  // The runtime returns the rich rejection directly when it rewrites input.
@@ -105,6 +84,56 @@ function fromCompiled(mod, schema, options) {
105
84
  return p.error ? false : isValid(p.value);
106
85
  },
107
86
  };
87
+ installExtension(api, schema, (base, ext) => {
88
+ const json = extendJSON(base, ext);
89
+ return {
90
+ // The verdict is the schema's and the check's. A failure lists the
91
+ // schema's errors, then the check's, read when somebody asks.
92
+ validate(data) {
93
+ const r = base.validate(data);
94
+ if (r.valid && ext.check(data)) return r;
95
+ return new ExtendedRejection(r, data, ext.errors);
96
+ },
97
+ isValidObject: (d) => base.isValidObject(d) && ext.check(d),
98
+ validateJSON: json.validateJSON,
99
+ isValidJSON: json.isValidJSON,
100
+ };
101
+ });
102
+ return api;
103
+ }
104
+
105
+ // validate() under a check, as the runtime reports it (ExtendedRejection in
106
+ // validator-core.js): the inner result's errors, then the check's.
107
+ class ExtendedRejection {
108
+ constructor(inner, data, collect) {
109
+ this.valid = false;
110
+ this._inner = inner;
111
+ this._data = data;
112
+ this._collect = collect;
113
+ this._errors = null;
114
+ }
115
+ toJSON() {
116
+ return { valid: false, errors: this.errors };
117
+ }
118
+ _ataRaw() {
119
+ const inner = this._inner;
120
+ const more = this._collect(this._data) || [];
121
+ const raw = inner.valid ? more : (typeof inner._ataRaw === 'function' ? inner._ataRaw() : inner.errors).concat(more);
122
+ return raw.length ? raw : [EXT_FALLBACK];
123
+ }
108
124
  }
125
+ Object.defineProperty(ExtendedRejection.prototype, 'errors', {
126
+ enumerable: true,
127
+ configurable: true,
128
+ get() {
129
+ if (this._errors === null) {
130
+ const inner = this._inner;
131
+ const more = this._collect(this._data) || [];
132
+ const all = inner.valid ? more : inner.errors.concat(more);
133
+ this._errors = all.length ? all : [EXT_FALLBACK];
134
+ }
135
+ return this._errors;
136
+ },
137
+ });
109
138
 
110
139
  module.exports = { fromCompiled, COMPILED_OPTIONS };
@@ -173,6 +173,7 @@ const DOC_BASE = 'https://ata-validator.com/e/'
173
173
  // reader sorts by without deriving it from the path. Empty when the path is
174
174
  // not into the root document.
175
175
  function ordinalField (ctx, schemaPath) {
176
+ if (ctx && ctx.noOrdinal) return ''
176
177
  const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, unescapeSp(schemaPath)) : null
177
178
  return o === null ? '' : `,_o:${o}`
178
179
  }
@@ -198,6 +199,8 @@ function unescapeSp (sp) {
198
199
  return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\(.)/g, '$1')
199
200
  }
200
201
 
202
+ const FRAME_INLINE_MAX = 120
203
+
201
204
  function buildErrorLiteral (opts) {
202
205
  const { keyword, format, schemaPath, sourceMap } = opts
203
206
  let code = (keyword === 'format' && format) ? codeFor('format', format) : codeFor(keyword)
@@ -214,10 +217,24 @@ function buildErrorLiteral (opts) {
214
217
  const line = Array.isArray(hit) ? hit[0] : hit.line
215
218
  const col = Array.isArray(hit) ? hit[1] : hit.col
216
219
  const text = Array.isArray(hit) ? hit[2] : hit.text
217
- frame = ',schemaSource:Object.freeze({file:' + JSON.stringify(sourceMap.file) +
218
- ',line:' + line +
219
- ',col:' + col +
220
- ',text:' + JSON.stringify(text) + '})'
220
+ if (sourceMap.frames && text.length > FRAME_INLINE_MAX) {
221
+ // A long source line is written once at module scope (see
222
+ // sourceFrameDecls in aot-impl.js) and the literal names it. Inlined,
223
+ // every error site carried the whole line, and a schema kept on one
224
+ // line is one line of all of it: 181 KB minified for a 2 KB schema.
225
+ // Short lines stay inline: gzip folds the repeats, and measured on
226
+ // the 50-field fixture the table cost 1206 gzipped bytes where the
227
+ // inline frames cost 696.
228
+ const key = line + ':' + col
229
+ let f = sourceMap.frames.get(key)
230
+ if (!f) { f = { line, col, text, index: sourceMap.frames.size }; sourceMap.frames.set(key, f) }
231
+ frame = ',schemaSource:__ataSS[' + f.index + ']'
232
+ } else {
233
+ frame = ',schemaSource:Object.freeze({file:' + JSON.stringify(sourceMap.file) +
234
+ ',line:' + line +
235
+ ',col:' + col +
236
+ ',text:' + JSON.stringify(text) + '})'
237
+ }
221
238
  }
222
239
  }
223
240
  return { codeStr: code, docUrl, frame }
@@ -4315,7 +4332,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4315
4332
  // closure parameters below the way the other entry points do.
4316
4333
  closureVars: [], closureVals: [],
4317
4334
  sourceMap: (sourceOpts && sourceOpts.sourceMap && sourceOpts.schemaFile)
4318
- ? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap }
4335
+ ? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap, frames: sourceOpts.frames || null }
4319
4336
  : null }
4320
4337
  ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
4321
4338
  const lines = []
@@ -4393,6 +4410,18 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
4393
4410
  factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
4394
4411
  '\n return{valid:_e.length===0,errors:_e}}'
4395
4412
  }
4413
+ // For the runtime, the literals take the legacy shape directly. `code` and
4414
+ // `docUrl` are there for standalone modules, which return errors as they
4415
+ // are; the runtime derives both from the keyword when it enriches, and
4416
+ // orders errors by schemaPath, so the literals carried three fields that
4417
+ // every read then copied the error to remove. The patterns are this
4418
+ // generator's own spelling; the collapse helper writes its fields with a
4419
+ // space and keeps them.
4420
+ if (sourceOpts && sourceOpts.runtimeShape) {
4421
+ const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '').replace(/,_o:\d+/g, '')
4422
+ body = shape(body)
4423
+ factoryBody = shape(factoryBody)
4424
+ }
4396
4425
  const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
4397
4426
  if (hoistable || needsGuardE) {
4398
4427
  const params = [...cvars, '__ataSafeRe']
@@ -5177,7 +5206,7 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5177
5206
  // Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
5178
5207
  // Avoids double-pass (jsFn → false → errFn runs same checks again).
5179
5208
  // Uses type-aware optimizations: after type check passes, skip guards.
5180
- function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
5209
+ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
5181
5210
  const inputSchema = schema
5182
5211
  schema = normalizePropertyNames(schema)
5183
5212
  // Same provably-local rule as the error generator above.
@@ -5257,7 +5286,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
5257
5286
  if (hasUnresolvableRef(schema, cRootDefs, cAnchors, schemaMap, new Set())) return null
5258
5287
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
5259
5288
 
5260
- const ctx = { varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
5289
+ // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
5290
+ const ctx = { noOrdinal: !!(opts && opts.runtimeShape), varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
5261
5291
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
5262
5292
  const lines = []
5263
5293
  try {
@@ -5392,7 +5422,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5392
5422
  if (pathVal !== undefined) {
5393
5423
  const spVal = unescapeSp(sp)
5394
5424
  ctx.closureVars.push(errVar)
5395
- const o = ordinalFor(ctx.rootSchema, spVal)
5425
+ const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
5396
5426
  const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
5397
5427
  if (o !== null) lit._o = o
5398
5428
  ctx.closureVals.push(Object.freeze(lit))
package/lib/rejections.js CHANGED
@@ -80,49 +80,53 @@ class RichRejection {
80
80
  return raw;
81
81
  }
82
82
  }
83
+ // Orders a raw error list and gives it the shape the caller asked for: the
84
+ // legacy key set, or enriched. Shared by both rejection classes below.
85
+ function presentErrors(raw, data, rawInput, self, root, enrich) {
86
+ if (needsOrdering(raw)) raw = sortErrorsBySchemaOrder(root, raw);
87
+ // The position map, resolved now that an error is actually being read.
88
+ let positions = null;
89
+ if (enrich && raw.length && rawInput != null) {
90
+ positions = self._pos().targeted(rawInput, wantedPointersFor(raw));
91
+ if (positions) self._posCache.reset();
92
+ }
93
+ // One options object for the whole list, not one per error.
94
+ const opts = enrich && raw.length
95
+ ? {
96
+ data,
97
+ positions,
98
+ schemaPositions: self._schemaPositions,
99
+ schemaFile: self._source ? self._source.path : undefined,
100
+ }
101
+ : null;
102
+ const cached = opts
103
+ ? raw.map((e) => enrich(e, opts))
104
+ // The v0.14 shape is a fixed key set; the ordering key the
105
+ // generated code carries is dropped from it here.
106
+ : raw.map(stripOrdinal);
107
+ // Correlation is published, never applied. Both halves of a typo pair
108
+ // stay in the array; `related` only says they are one mistake, so a
109
+ // wrong pairing costs a sentence rather than a hidden violation.
110
+ if (enrich && cached.length > 1) attachRelated(cached);
111
+ // No diagnostic payload here. validate(data) is the library hot path,
112
+ // and attaching one cost about 100 ns per rejection for a consumer
113
+ // that never renders. The text path attaches it, and a renderer given
114
+ // `{ data }` builds frames for object input on request.
115
+ return cached;
116
+ }
117
+
83
118
  Object.defineProperty(RichRejection.prototype, 'errors', {
84
119
  enumerable: true,
85
120
  configurable: true,
86
121
  get() {
87
122
  if (this._cached === null) {
88
- const self = this._self;
89
- const enrich = this._enrich;
90
- let raw = this._result.errors || [];
91
- if (needsOrdering(raw)) raw = sortErrorsBySchemaOrder(this._root, raw);
92
- // The position map, resolved now that an error is actually being read.
93
- let positions = null;
94
- if (enrich && raw.length && this._rawInput != null) {
95
- positions = self._pos().targeted(this._rawInput, wantedPointersFor(raw));
96
- if (positions) self._posCache.reset();
97
- }
98
- // One options object for the whole list, not one per error.
99
- const opts = enrich && raw.length
100
- ? {
101
- data: this._data,
102
- positions,
103
- schemaPositions: self._schemaPositions,
104
- schemaFile: self._source ? self._source.path : undefined,
105
- }
106
- : null;
107
- const cached = opts
108
- ? raw.map((e) => enrich(e, opts))
109
- // The v0.14 shape is a fixed key set; the ordering key the
110
- // generated code carries is dropped from it here.
111
- : raw.map(stripOrdinal);
112
- // Correlation is published, never applied. Both halves of a typo pair
113
- // stay in the array; `related` only says they are one mistake, so a
114
- // wrong pairing costs a sentence rather than a hidden violation.
115
- if (enrich && cached.length > 1) attachRelated(cached);
116
- // No diagnostic payload here. validate(data) is the library hot path,
117
- // and attaching one cost about 100 ns per rejection for a consumer
118
- // that never renders. The text path attaches it, and a renderer given
119
- // `{ data }` builds frames for object input on request.
120
- this._cached = cached;
123
+ this._cached = presentErrors(this._result.errors || [], this._data, this._rawInput, this._self, this._root, this._enrich);
121
124
  }
122
125
  return this._cached;
123
126
  },
124
127
  });
125
128
 
129
+
126
130
  // The rejection validateJSON returns. Everything the text path adds over
127
131
  // validate(data), the value tree enrichment needs, the position map, the
128
132
  // diagnostic payload, happens on first access to `.errors`. Reading `.valid`
@@ -256,7 +260,10 @@ function wantedPointersFor (errors) {
256
260
  const COLLAPSE_CODES = new Set(['ATA4001', 'ATA4002', 'ATA4003']);
257
261
  function stripOrdinal(e) {
258
262
  if (e === null || typeof e !== 'object') return e;
259
- if (e._o === undefined && e.docUrl === undefined && (e.code === undefined || COLLAPSE_CODES.has(e.code)) && !e.branchErrors) return e;
263
+ // Generated code keeps errors at a fixed path as frozen literals shared by
264
+ // every call. The caller gets its own copy, as it always has, so an error it
265
+ // edits (a translated message) is its own and editing it does not throw.
266
+ if (e._o === undefined && e.docUrl === undefined && (e.code === undefined || COLLAPSE_CODES.has(e.code)) && !e.branchErrors) return Object.isFrozen(e) ? { ...e } : e;
260
267
  const out = {};
261
268
  for (const k in e) {
262
269
  if (k === '_o' || k === 'docUrl') continue;
@@ -364,4 +371,4 @@ function attachRelated (errors) {
364
371
  return errors;
365
372
  }
366
373
 
367
- module.exports = { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, attachRelated, sortErrorsBySchemaOrder, stripOrdinal, wantedPointersFor };
374
+ module.exports = { LazyRejection, RichRejection, presentErrors, needsOrdering, LazyJsonRejection, _enrichLazy, attachRelated, sortErrorsBySchemaOrder, stripOrdinal, wantedPointersFor };
@@ -42,7 +42,11 @@ function caretSuffix (d, raw, useColor) {
42
42
  function renderOne (d, useColor, opts) {
43
43
  const lines = [];
44
44
  const width = terminalWidth();
45
- const gutter = 3;
45
+ // The bar sits one column past the widest line number, so the source line,
46
+ // the blank rows and the caret row share it and a caret lands on the first
47
+ // character of the value, not one column to its left.
48
+ const bar = (line) => ' '.repeat(Math.max(3, String(line).length + 1)) + '|';
49
+ const src = (line, text) => String(line).padStart(Math.max(2, String(line).length)) + ' | ' + text;
46
50
 
47
51
  // Headline carries the code when there is one. A code-less error, which the
48
52
  // LazyRejection fallback can produce, must not render as "error[undefined]".
@@ -57,12 +61,12 @@ function renderOne (d, useColor, opts) {
57
61
  if (raw && raw.schemaSource) {
58
62
  const f = trimCwd(raw.schemaSource.file, opts.cwd);
59
63
  lines.push(` --> ${color(useColor, ANSI.cyan, `${f}:${raw.schemaSource.line}:${raw.schemaSource.col}`)}`);
60
- lines.push(' |');
61
- const sln = String(raw.schemaSource.line).padStart(2, ' ');
62
- lines.push(` ${sln} | ${truncateLine(raw.schemaSource.text, width - 8)}`);
64
+ const sl = raw.schemaSource.line;
65
+ lines.push(bar(sl));
66
+ lines.push(src(sl, truncateLine(raw.schemaSource.text, width - 8)));
63
67
  const inlineHint = raw.expected ? ' ' + color(useColor, ANSI.dim, `expected ${raw.expected}`) : '';
64
- lines.push(caretLine(raw.schemaSource.col, 1, gutter) + inlineHint);
65
- lines.push(' |');
68
+ lines.push(caretLine(raw.schemaSource.col, 1, bar(sl).length - 1) + inlineHint);
69
+ lines.push(bar(sl));
66
70
  }
67
71
 
68
72
  // Data location, always, even without a frame. This is the single most
@@ -70,11 +74,11 @@ function renderOne (d, useColor, opts) {
70
74
  if (d.frame) {
71
75
  const where = `input:${d.frame.line}:${d.frame.col}`;
72
76
  lines.push(` --> ${color(useColor, ANSI.cyan, where)} ${color(useColor, ANSI.dim, '(' + d.dotted + ')')}`);
73
- lines.push(' |');
74
- const ln = String(d.frame.line).padStart(2, ' ');
75
- lines.push(` ${ln} | ${truncateLine(d.frame.text, width - 8)}`);
76
- lines.push(caretLine(d.frame.col, d.frame.length, gutter) + caretSuffix(d, raw, useColor));
77
- lines.push(' |');
77
+ const fl = d.frame.line;
78
+ lines.push(bar(fl));
79
+ lines.push(src(fl, truncateLine(d.frame.text, width - 8)));
80
+ lines.push(caretLine(d.frame.col, d.frame.length, bar(fl).length - 1) + caretSuffix(d, raw, useColor));
81
+ lines.push(bar(fl));
78
82
  } else {
79
83
  lines.push(` --> ${color(useColor, ANSI.cyan, 'at ' + d.dotted)}`);
80
84
  }
@@ -156,6 +156,14 @@ function ordinalFor(rootSchema, schemaPath) {
156
156
  const map = ordinals(rootSchema);
157
157
  const hit = map.get(schemaPath);
158
158
  if (hit !== undefined) return hit;
159
+ // A path that is not a node is answered from the cache below, filled by the
160
+ // walk after it: every error behind a `$ref`, which is every error of a
161
+ // schema the interpreter answers through references, used to walk its path
162
+ // back segment by segment on every read.
163
+ let misses = _missCache.get(rootSchema);
164
+ if (misses === undefined) { misses = new Map(); _missCache.set(rootSchema, misses); }
165
+ const known = misses.get(schemaPath);
166
+ if (known !== undefined) return known;
159
167
  // Walk back to the longest prefix that exists. Paths through a keyword's
160
168
  // value that is a primitive, or through a key the schema does not have,
161
169
  // land on the nearest enclosing node.
@@ -163,16 +171,24 @@ function ordinalFor(rootSchema, schemaPath) {
163
171
  // behind a reference sort where the `$ref` is declared among its siblings;
164
172
  // rankFor orders them among themselves.
165
173
  let p = schemaPath;
174
+ let found = 0;
166
175
  while (true) {
167
176
  const cut = p.lastIndexOf('/');
168
- if (cut < 0) return 0;
177
+ if (cut < 0) break;
169
178
  p = p.slice(0, cut);
170
179
  const o = map.get(p);
171
180
  if (o !== undefined) {
172
181
  const r = map.get(p + '/$ref');
173
- return r !== undefined ? r : o;
182
+ found = r !== undefined ? r : o;
183
+ break;
174
184
  }
175
185
  }
186
+ // Bounded: behind a recursive reference the paths grow with the document's
187
+ // depth, and the cache must not grow with them.
188
+ if (misses.size < MISS_CACHE_LIMIT) misses.set(schemaPath, found);
189
+ return found;
176
190
  }
191
+ const _missCache = new WeakMap();
192
+ const MISS_CACHE_LIMIT = 4096;
177
193
 
178
194
  module.exports = { rankFor, ordinalFor, unescapePointerSegment };
@@ -292,7 +292,7 @@ function resolveSchemaByPath(rootSchema, schemaPath) {
292
292
  //
293
293
  // A failing route sees the same handful of schemaPaths over and over, which is
294
294
  // what makes the first one worth having.
295
- const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy } = require('./rejections');
295
+ const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, presentErrors, needsOrdering, sortErrorsBySchemaOrder } = require('./rejections');
296
296
 
297
297
  // Text that does not parse, reported the same way on every path and platform:
298
298
  // ATA9001 with the parser's own message. The native addon used to say only
@@ -991,20 +991,28 @@ class Validator {
991
991
  };
992
992
  const _buildCombined = () => {
993
993
  if (jsCombinedFn !== undefined) return;
994
- jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, this._userFormats) || null;
994
+ jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, this._userFormats, { runtimeShape: true }) || null;
995
995
  _upgradeCacheEntry();
996
996
  };
997
997
  const _buildErr = () => {
998
998
  if (jsErrFn !== undefined) return;
999
- jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, this._userFormats) || null;
999
+ jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, this._userFormats, { runtimeShape: true }) || null;
1000
1000
  _upgradeCacheEntry();
1001
1001
  };
1002
1002
 
1003
+ // What validate() returns for a document already known to fail, without
1004
+ // deciding again, when the codegen paths can say (see index.js). Kept in
1005
+ // locals and on this object rather than on the instance: every call reads
1006
+ // `validate` off the instance, and three more own properties made that
1007
+ // read slower across the suite's validators.
1008
+ let _rejectBase = null, _rejectPresented = null, _rejectPlain = false;
1003
1009
  if (jsFn) {
1004
- _codegen.installPaths.call(this, {
1010
+ const _cg = {
1005
1011
  jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr,
1006
- combined: () => jsCombinedFn, err: () => jsErrFn,
1007
- });
1012
+ combined: () => jsCombinedFn, err: () => jsErrFn, rejectBase: null,
1013
+ };
1014
+ _codegen.installPaths.call(this, _cg);
1015
+ _rejectBase = _cg.rejectBase;
1008
1016
  } else {
1009
1017
  // No JS codegen: the interpreted engine, with or without the addon. The
1010
1018
  // buffer APIs come from _installBufferApis on first use, as they do on
@@ -1030,7 +1038,9 @@ class Validator {
1030
1038
  : (preprocess
1031
1039
  ? (data) => { preprocess(data); return interp.validate(data); }
1032
1040
  : (data) => interp.validate(data));
1033
- this.validate = run;
1041
+ this.validate = this._verbose ? _verboseWrap(run, schemaObj) : run;
1042
+ // Reading errors runs the interpreter once, directly; see _rejectBase.
1043
+ if (!preprocess && !options.abortEarly && !this._verbose) _rejectBase = (data) => interp.validate(data);
1034
1044
  _bindVerdict(this, this._fastVerdict
1035
1045
  ? this._fastVerdict
1036
1046
  : (data) => run(data).valid);
@@ -1059,8 +1069,7 @@ class Validator {
1059
1069
  const enrich = this._richErrors ? _enrichLazy : null;
1060
1070
  const root = this._schemaObj;
1061
1071
  const self = this;
1062
- this.validate = (data) => {
1063
- const result = inner(data);
1072
+ const present = (result, data) => {
1064
1073
  // abortEarly returns the shared ATA9000 stub; preserve it as-is so the
1065
1074
  // perf fast path stays allocation-free and the documented code stays stable.
1066
1075
  if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
@@ -1076,6 +1085,13 @@ class Validator {
1076
1085
  }
1077
1086
  return result;
1078
1087
  };
1088
+ this.validate = (data) => present(inner(data), data);
1089
+ // The same presentation for a document already known to fail.
1090
+ const rejectBase = _rejectBase;
1091
+ _rejectPresented = rejectBase ? (data) => present(rejectBase(data), data) : null;
1092
+ // True while nothing but this layer stands between a rejection and the
1093
+ // caller; the lazy layer below can then build the rejection itself.
1094
+ _rejectPlain = !!rejectBase;
1079
1095
 
1080
1096
  // validateJSON also enriches: set _lastRawInput so the position cache
1081
1097
  // can lazily build a map for dataFrame attachment. Only validateJSON
@@ -1149,6 +1165,8 @@ class Validator {
1149
1165
  return result;
1150
1166
  };
1151
1167
  if (this.validate) this.validate = wrap(this.validate);
1168
+ if (_rejectPresented) _rejectPresented = wrap(_rejectPresented);
1169
+ _rejectPlain = false;
1152
1170
  if (this.validateJSON) this.validateJSON = wrap(this.validateJSON);
1153
1171
  // validateAndParse routes through self.validate on the codegen path, but
1154
1172
  // the native-only path returns directly from the addon — wrap it so both
@@ -1185,7 +1203,13 @@ class Validator {
1185
1203
  const _vx = this._validateTail !== null ? this._validateTail() : null;
1186
1204
  let _vxApplied = false;
1187
1205
  if (this._fastVerdict && !preprocess && !options.abortEarly && this.validate) {
1188
- const _full = this.validate;
1206
+ // The errors of a document the verdict rejected come from the rejection
1207
+ // path directly, not from validate() deciding a second time. Not with a
1208
+ // check from _extendValidate: that check can reject what the schema
1209
+ // accepts, and then the schema's own errors must come from a verdict.
1210
+ // Not with removeAdditional's fused pass either, which edits the
1211
+ // document before the errors are read.
1212
+ const _full = (_vx === null && !fusedRemove && _rejectPresented) ? _rejectPresented : this.validate;
1189
1213
  const _fast = _vx ? _fuseTail(this._fastVerdict, _vx.check) : this._fastVerdict;
1190
1214
  const _extra = _vx ? _vx.errors : null;
1191
1215
  _vxApplied = true;
@@ -1198,11 +1222,19 @@ class Validator {
1198
1222
  // verdict and say so rather than inventing a specific error.
1199
1223
  return own || _verdictFallback;
1200
1224
  };
1201
- const _buildErrors = (data) => {
1225
+ // The common case: the verdict decides, and reading `errors` runs the
1226
+ // error function and presents its list directly. Through the layers it
1227
+ // took a second verdict and a second rejection object, about 75 ns of a
1228
+ // 96 ns read on a two-error document. validate() below is the same code
1229
+ // either way, so reading only `.valid` costs what it did.
1230
+ const _plain = (_vx === null && !fusedRemove && _rejectPlain)
1231
+ ? _plainBuilders(_rejectBase, this._richErrors ? _enrichLazy : null, this._schemaObj, this, _verdictFallback)
1232
+ : null;
1233
+ const _buildErrors = _plain !== null ? _plain.errors : (data) => {
1202
1234
  const r = _full(data);
1203
1235
  return _withExtra((r && r.valid === false && r.errors && r.errors.length) ? r.errors : null, data);
1204
1236
  };
1205
- const _buildRawErrors = (data) => {
1237
+ const _buildRawErrors = _plain !== null ? _plain.raw : (data) => {
1206
1238
  const r = _full(data);
1207
1239
  let raw = null;
1208
1240
  if (r && r.valid === false) {
@@ -1700,6 +1732,60 @@ const _VERDICT_DISAGREES = Object.freeze({
1700
1732
  valid: false,
1701
1733
  errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
1702
1734
  });
1735
+ // The error builders of the common rejection path (see _ensureCompiled): run
1736
+ // the error function behind `reject` for a document the verdict rejected, and
1737
+ // order and present its list. Out here so the closures capture only this.
1738
+ function _plainBuilders(reject, enrich, root, self, fallback) {
1739
+ const rawOf = (data) => {
1740
+ const r = reject(data);
1741
+ const errors = r && r.errors;
1742
+ return errors && errors.length ? errors : fallback;
1743
+ };
1744
+ return {
1745
+ errors: (data) => presentErrors(rawOf(data), data, null, self, root, enrich),
1746
+ raw: (data) => {
1747
+ const raw = rawOf(data);
1748
+ return needsOrdering(raw) ? sortErrorsBySchemaOrder(root, raw) : raw;
1749
+ },
1750
+ };
1751
+ }
1752
+
1753
+ // Verbose mode: populate parentSchema, schema and data on each error, the
1754
+ // three fields the default error shape carries under the same option. `data`
1755
+ // is the value the error points at; without it a caller has to walk the
1756
+ // document by the instance path itself, which is what one migration ended up
1757
+ // writing by hand. Errors may be frozen, so they are cloned with the extra
1758
+ // fields. Both engines go through this; the interpreted one used to skip it,
1759
+ // so the same validator reported different fields depending on which engine
1760
+ // answered.
1761
+ function _verboseWrap(inner, root) {
1762
+ const { resolvePointer } = require('./pointer.js');
1763
+ return (data) => {
1764
+ const result = inner(data);
1765
+ if (result && !result.valid && result.errors) {
1766
+ const enriched = result.errors.map((err) => {
1767
+ if (!err || err.parentSchema !== undefined) return err;
1768
+ const parentSchema = resolveSchemaByPath(root, err.schemaPath);
1769
+ // The last segment of the schema path is the keyword that
1770
+ // failed, so its value on the parent is that keyword's schema.
1771
+ const sp = typeof err.schemaPath === 'string' ? err.schemaPath : '';
1772
+ const last = sp.slice(sp.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~');
1773
+ const keywordSchema = (parentSchema !== null && typeof parentSchema === 'object' && last)
1774
+ ? parentSchema[last]
1775
+ : undefined;
1776
+ return {
1777
+ ...err,
1778
+ parentSchema,
1779
+ schema: keywordSchema,
1780
+ data: resolvePointer(data, err.instancePath, undefined),
1781
+ };
1782
+ });
1783
+ return { valid: false, errors: enriched };
1784
+ }
1785
+ return result;
1786
+ };
1787
+ }
1788
+
1703
1789
  function _mustReject(r) {
1704
1790
  return r && r.valid === false ? r : _VERDICT_DISAGREES;
1705
1791
  }
@@ -2063,4 +2149,4 @@ module.exports = {
2063
2149
  // surface of `require('ata-validator')` is what it was.
2064
2150
  Object.defineProperty(module.exports, '_registerCodegen', { value: _registerCodegen, enumerable: false });
2065
2151
  // What the compiled paths in index.js need from this module.
2066
- Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _mustReject, _rememberInstance, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
2152
+ Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.39.1';
10
+ module.exports = '1.40.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.39.1",
3
+ "version": "1.40.0",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -43,6 +43,11 @@
43
43
  "import": "./compiled.mjs",
44
44
  "require": "./compiled.js"
45
45
  },
46
+ "./compiled-verdict": {
47
+ "types": "./compiled-verdict.d.ts",
48
+ "import": "./compiled-verdict.mjs",
49
+ "require": "./compiled-verdict.js"
50
+ },
46
51
  "./package.json": "./package.json"
47
52
  },
48
53
  "sideEffects": false,
@@ -59,7 +64,7 @@
59
64
  "release:check": "node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
60
65
  "build": "cmake-js build --target ata",
61
66
  "rebuild": "cmake-js rebuild --target ata",
62
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_text_path_errors.js && node tests/test_compiled_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_error_list_differential.js && node tests/test_output_format.js && node tests/test_standard_json_schema.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_esm_exports.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_native_lazy.js && node tests/test_buffer_apis_without_native.js && node tests/test_cold_start_modules.js && node tests/test_safe_regex_embed.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_regex_ecma_whitespace.js && node tests/test_regex_native_linear.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_first_rejection_builds_one.js && node tests/test_container_local.js && node tests/test_one_shot_validate.js && node tests/test_fixed_pattern_inline.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_ref_annotation_siblings.js && node tests/test_gate_wrapped_schemas.js && node tests/test_pattern_message_escape.js && node tests/test_nested_defaults_engines.js && node tests/test_nested_coercion_parity.js && node tests/test_preprocess_cache.js && node tests/test_large_schema_codegen.js && node tests/test_base_uri.js && node tests/test_hoisted_def_recursion.js && node tests/test_alias_definitions.js && node tests/test_property_names_ref.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_unevaluated_differential.js && node tests/fuzz_differential.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_ref_error_order.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_lite_parity.js && node tests/test_lite_bundle_size.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node tests/test_collect_evaluated.js && node tests/test_differential_tiers.js && node tests/test_draft7.js && node tests/test_dual_path.js && node tests/test_keywords_codegen.js && node tests/test_ref_cross.js && node tests/test_shape_classifier.js && node tests/test_tier0.js && node benchmark/bench_aot_size.mjs",
67
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_text_path_errors.js && node tests/test_compiled_parity.js && node tests/test_compiled_extension_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_error_list_differential.js && node tests/test_output_format.js && node tests/test_standard_json_schema.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_esm_exports.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_native_lazy.js && node tests/test_buffer_apis_without_native.js && node tests/test_cold_start_modules.js && node tests/test_safe_regex_embed.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_regex_ecma_whitespace.js && node tests/test_regex_native_linear.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_first_rejection_builds_one.js && node tests/test_container_local.js && node tests/test_one_shot_validate.js && node tests/test_fixed_pattern_inline.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_ref_annotation_siblings.js && node tests/test_gate_wrapped_schemas.js && node tests/test_pattern_message_escape.js && node tests/test_nested_defaults_engines.js && node tests/test_nested_coercion_parity.js && node tests/test_preprocess_cache.js && node tests/test_large_schema_codegen.js && node tests/test_base_uri.js && node tests/test_hoisted_def_recursion.js && node tests/test_alias_definitions.js && node tests/test_property_names_ref.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_unevaluated_differential.js && node tests/fuzz_differential.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_ref_error_order.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_lite_parity.js && node tests/test_lite_bundle_size.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_aot_source_frames.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node tests/test_collect_evaluated.js && node tests/test_differential_tiers.js && node tests/test_draft7.js && node tests/test_dual_path.js && node tests/test_keywords_codegen.js && node tests/test_ref_cross.js && node tests/test_shape_classifier.js && node tests/test_tier0.js && node benchmark/bench_aot_size.mjs",
63
68
  "bench:size": "node benchmark/bench_aot_size.mjs",
64
69
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
65
70
  "test:compat": "node tests/test_compat.js",
@@ -130,18 +135,21 @@
130
135
  "compiled.js",
131
136
  "compiled.mjs",
132
137
  "compiled.d.ts",
138
+ "compiled-verdict.js",
139
+ "compiled-verdict.mjs",
140
+ "compiled-verdict.d.ts",
133
141
  "bin/",
134
142
  "README.md",
135
143
  "LICENSE"
136
144
  ],
137
145
  "optionalDependencies": {
138
- "@ata-validator/native-darwin-arm64": "1.39.1",
139
- "@ata-validator/native-darwin-x64": "1.39.1",
140
- "@ata-validator/native-linux-arm64-gnu": "1.39.1",
141
- "@ata-validator/native-linux-arm64-musl": "1.39.1",
142
- "@ata-validator/native-linux-x64-gnu": "1.39.1",
143
- "@ata-validator/native-linux-x64-musl": "1.39.1",
144
- "@ata-validator/native-win32-x64": "1.39.1"
146
+ "@ata-validator/native-darwin-arm64": "1.40.0",
147
+ "@ata-validator/native-darwin-x64": "1.40.0",
148
+ "@ata-validator/native-linux-arm64-gnu": "1.40.0",
149
+ "@ata-validator/native-linux-arm64-musl": "1.40.0",
150
+ "@ata-validator/native-linux-x64-gnu": "1.40.0",
151
+ "@ata-validator/native-linux-x64-musl": "1.40.0",
152
+ "@ata-validator/native-win32-x64": "1.40.0"
145
153
  },
146
154
  "peerDependencies": {
147
155
  "yaml": "^2.0.0"