ata-validator 1.41.0 → 1.43.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/index.js CHANGED
@@ -35,6 +35,65 @@ class TextRejection {
35
35
  }
36
36
  }
37
37
 
38
+ // Errors built enriched where they fail by the rich combined function (see
39
+ // makeRich in lib/enrich-site.js), for a validator whose errors are being read
40
+ // with richErrors on. Presenting them is the ordering and the `related` links
41
+ // presentErrors applies after enriching. Where the rich function does not
42
+ // reject (the data changed under the read), the plain fallback is presented
43
+ // the usual way.
44
+ const { needsOrdering: _needsOrdering, sortErrorsBySchemaOrder: _sortErrors, attachRelated: _attachRelated } = require('./lib/rejections');
45
+ let _enrichFnIdx = null;
46
+ function _enrichOne(e, data) {
47
+ if (_enrichFnIdx === null) _enrichFnIdx = require('./lib/enrich-error').enrich;
48
+ return _enrichFnIdx(e, data !== undefined ? { data } : null);
49
+ }
50
+ function richBuilder(combRich, root, presentFallback, fallback, plain, cleanPlain) {
51
+ if (plain) {
52
+ // The runtime-shape combined function writes each error fresh and in the
53
+ // plain shape: no shared literal, no ordinal, no code or docUrl, no
54
+ // collapsed branches (lib/js-compiler.js, compileToJSCombined with
55
+ // runtimeShape). So without enrichment the list is the caller's as it is,
56
+ // once in schema order. Checking each error for those fields instead cost
57
+ // a megamorphic load per field per error across many schemas.
58
+ // tests/test_error_read_paths.js holds every error handed out to being
59
+ // the caller's own.
60
+ return (data) => {
61
+ const r = combRich(data);
62
+ if (r.valid || !r.errors || !r.errors.length) return presentFallback(fallback, data);
63
+ if (cleanPlain) return _needsOrdering(r.errors) ? _sortErrors(root, r.errors) : r.errors;
64
+ return presentFallback(r.errors, data);
65
+ };
66
+ }
67
+ return (data) => {
68
+ const r = combRich(data);
69
+ if (r.valid || !r.errors || !r.errors.length) return presentFallback(fallback, data);
70
+ const out = _needsOrdering(r.errors) ? _sortErrors(root, r.errors) : r.errors;
71
+ // A collapsed oneOf or anyOf comes out plain (no docUrl) and is enriched
72
+ // here, its branch errors with it, as presentErrors would.
73
+ for (let i = 0; i < out.length; i++) {
74
+ if (out[i].docUrl === undefined) out[i] = _enrichOne(out[i], data);
75
+ }
76
+ if (out.length > 1) _attachRelated(out);
77
+ return out;
78
+ };
79
+ }
80
+
81
+ // The rejection validate() returns once it is the one-pass combined function
82
+ // (resultShape in compileToJSCombined): the errors are already built, plain,
83
+ // fresh and in schema order. Serialises as the lazy rejection does.
84
+ class EagerRejection {
85
+ constructor(errors) {
86
+ this.valid = false;
87
+ this.errors = errors;
88
+ }
89
+ toJSON() {
90
+ return { valid: false, errors: this.errors };
91
+ }
92
+ _ataRaw() {
93
+ return this.errors;
94
+ }
95
+ }
96
+
38
97
  function installCodegenPaths (ctx) {
39
98
  const { ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindVerdict, _jsonSyntaxRejection, _mustReject, _verboseWrap, getNative, isV1Dialect, resolveSchemaByPath } = core._internals;
40
99
  const { jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr } = ctx;
@@ -131,6 +190,36 @@ function installCodegenPaths (ctx) {
131
190
  return _safeCombined;
132
191
  };
133
192
 
193
+ // The error function on its own, probed the way errOnly probes it, or null.
194
+ const errFnIfSafe = () => {
195
+ _buildErr();
196
+ const efn = ctx.err();
197
+ if (!efn) return null;
198
+ try { efn({}, true); } catch { return null; }
199
+ return (x) => efn(x, true);
200
+ };
201
+
202
+ // The rich combined function, built and probed the way the plain one is,
203
+ // and only where the plain one passed its probe.
204
+ let _richProbed = false;
205
+ let _safeRich = null;
206
+ const richIfSafe = () => {
207
+ if (_richProbed) return _safeRich;
208
+ _richProbed = true;
209
+ if (!combinedIfSafe() || !ctx.buildRich) return null;
210
+ const fn = ctx.buildRich();
211
+ if (fn) {
212
+ try {
213
+ const probe = {};
214
+ if (schemaObj && schemaObj.properties) for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
215
+ if (schemaObj && schemaObj.if && schemaObj.if.properties) for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
216
+ fn(probe); fn({}); fn(null); fn(0);
217
+ _safeRich = fn;
218
+ } catch {}
219
+ }
220
+ return _safeRich;
221
+ };
222
+
134
223
  // What the hybrid path hands to its error slot: the combined function
135
224
  // when it is usable, since it validates and collects in one pass, and
136
225
  // the error generator otherwise. Same order the eager code chose, just
@@ -227,6 +316,38 @@ function installCodegenPaths (ctx) {
227
316
  // and used to call validate() to get the errors, which ran the verdict a
228
317
  // second time and built a second rejection around it.
229
318
  if (!preprocess) ctx.rejectBase = onReject;
319
+ // The rich combined function, for a validator whose errors are being read
320
+ // with richErrors on. See _ensureCompiled.
321
+ if (!preprocess) {
322
+ ctx.richBuilder = richBuilder;
323
+ // The plain combined function stands in where no rich one is built
324
+ // (richErrors off, or a schema source to frame errors from): its errors
325
+ // are presented, and enriched when asked, the usual way, without the
326
+ // rejection layers between.
327
+ // Where the plain combined function declines (oneOf, anyOf, an
328
+ // additionalProperties schema), the error function stands in, called
329
+ // directly rather than through the rejection layers. Its errors can be
330
+ // collapsed branches that still need presenting; only the combined
331
+ // function's are already in their final plain shape (isCleanShape).
332
+ ctx.oneShotRich = ctx.buildRich ? richIfSafe : () => combinedIfSafe() || errFnIfSafe();
333
+ ctx.oneShotPlain = !ctx.buildRich;
334
+ ctx.isCleanShape = (fn) => fn !== null && fn === _safeCombined;
335
+ ctx.isCombined = (fn) => fn !== null && fn === _safeCombined;
336
+ // validate() as the combined function in its result shape: built and
337
+ // probed here, null when it declines or throws on the probe.
338
+ ctx.onePassOf = (empty, fallback) => {
339
+ // A collapsed branch error is built in its final plain shape here
340
+ // (no ordering key in the runtime shape); its branch errors are put
341
+ // in order with the rest.
342
+ const sort = (errs) => (_needsOrdering(errs) ? _sortErrors(schemaObj, errs) : errs);
343
+ try {
344
+ const fn = jsCompiler.compileToJSCombined(schemaObj, VALID_RESULT, this._schemaMap.size > 0 ? this._schemaMap : null, this._userFormats, { runtimeShape: true, resultShape: { Rejection: EagerRejection, empty, sort, fallback, verdict: jsFn } });
345
+ if (!fn) return null;
346
+ fn({}); fn(null); fn(0);
347
+ return fn;
348
+ } catch { return null; }
349
+ };
350
+ }
230
351
  } else {
231
352
  // No hybrid factory, so the assembly needs the function itself rather
232
353
  // than a reference it can call later: build it now.
@@ -256,6 +377,7 @@ function installCodegenPaths (ctx) {
256
377
  // The verbose fields are added here, so a path around this layer would
257
378
  // miss them.
258
379
  ctx.rejectBase = null;
380
+ ctx.oneShotRich = null;
259
381
  this.validate = _verboseWrap(this.validate, this._schemaObj);
260
382
  }
261
383
  // The verdict methods answer validate()'s question without building the
@@ -381,7 +503,7 @@ function installCodegenPaths (ctx) {
381
503
  // The verdict function alone, for isValidObject() before a full compile: the
382
504
  // error and combined generators are left for the first rejection.
383
505
  installCodegenPaths.compileVerdict = function compileVerdict () {
384
- const { compileCacheKey, _compileCache, _bindVerdict, _rememberInstance } = core._internals;
506
+ const { compileCacheKey, _compileCache, _compileCacheSet, _bindVerdict, _rememberInstance } = core._internals;
385
507
  if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
386
508
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
387
509
  const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
@@ -410,7 +532,7 @@ installCodegenPaths.compileVerdict = function compileVerdict () {
410
532
  // validator that later reuses this entry reports the same engine it
411
533
  // would have compiled to.
412
534
  if (!uf && !this._keywords) {
413
- if (!cached) _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
535
+ if (!cached) _compileCacheSet(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
414
536
  else cached.jsFn = jsFn;
415
537
  }
416
538
  }
@@ -40,25 +40,35 @@ function __ataScore (errs) {
40
40
  return errs.length * 100 + s; // primary: count, secondary: severity
41
41
  }
42
42
 
43
- // `br` is one entry per branch in declaration order: {valid, errors, title}.
43
+ // `br` is one entry per branch in declaration order: {valid, errors} and a
44
+ // `title`, or the titles in `titles` when given (the generated code passes
45
+ // each branch's result as it is rather than copying it to add one).
44
46
  // Returns the single error to report, or null when the keyword is satisfied.
45
- function __ataCollapse (kw, br, pp, sp, o) {
46
- const pass = br.filter((b) => b.valid);
47
+ // oneOf passed by more than one branch; generated code builds it from a count.
48
+ function __ataMulti (passIdx, total, pp, sp, o) {
49
+ const multi = {
50
+ code: 'ATA4002',
51
+ keyword: 'oneOf',
52
+ instancePath: pp || '',
53
+ path: pp || '',
54
+ schemaPath: sp,
55
+ message: 'value matched ' + passIdx.length + ' of ' + total + ' oneOf variants, expected exactly one',
56
+ params: { passingSchemas: passIdx },
57
+ };
58
+ if (o !== null) Object.defineProperty(multi, '_o', { value: o });
59
+ return multi;
60
+ }
61
+
62
+ function __ataCollapse (kw, br, pp, sp, o, titles) {
63
+ let passN = 0;
64
+ for (let i = 0; i < br.length; i++) if (br[i].valid) passN++;
65
+ const pass = { length: passN };
47
66
  if (kw === 'oneOf') {
48
67
  if (pass.length === 1) return null;
49
68
  if (pass.length > 1) {
50
69
  const passIdx = [];
51
70
  for (let i = 0; i < br.length; i++) if (br[i].valid) passIdx.push(i);
52
- return {
53
- code: 'ATA4002',
54
- keyword: 'oneOf',
55
- instancePath: pp || '',
56
- path: pp || '',
57
- schemaPath: sp,
58
- _o: o,
59
- message: 'value matched ' + pass.length + ' of ' + br.length + ' oneOf variants, expected exactly one',
60
- params: { passingSchemas: passIdx },
61
- };
71
+ return __ataMulti(passIdx, br.length, pp, sp, o);
62
72
  }
63
73
  } else if (pass.length >= 1) {
64
74
  return null;
@@ -70,17 +80,21 @@ function __ataCollapse (kw, br, pp, sp, o) {
70
80
  if (s < bs) { bs = s; bi = i; }
71
81
  }
72
82
  const best = br[bi];
73
- return {
83
+ // The ordering key, when there is one, is not enumerable: the error is
84
+ // otherwise in the shape it is handed out in, and a caller that reads it
85
+ // as it comes (the combined function's one-pass result) does not copy it.
86
+ const none = {
74
87
  code: kw === 'oneOf' ? 'ATA4001' : 'ATA4003',
75
88
  keyword: kw,
76
89
  instancePath: pp || '',
77
90
  path: pp || '',
78
91
  schemaPath: sp,
79
- _o: o,
80
92
  message: 'value matched 0 of ' + br.length + ' ' + kw + ' variants',
81
- params: { variants: br.length, closest: bi, closestName: best.title || ('variant ' + (bi + 1)) },
93
+ params: { variants: br.length, closest: bi, closestName: (titles !== undefined ? titles[bi] : best.title) || ('variant ' + (bi + 1)) },
82
94
  branchErrors: best.errors,
83
95
  };
96
+ if (o !== null) Object.defineProperty(none, '_o', { value: o });
97
+ return none;
84
98
  }
85
99
 
86
100
  // The named-argument form the interpreter and the tests call.
@@ -88,12 +102,13 @@ function collapseBranches ({ keyword, branchResults, parentPath, parentSchemaPat
88
102
  return __ataCollapse(keyword, branchResults, parentPath, parentSchemaPath, ordinal === undefined ? null : ordinal);
89
103
  }
90
104
 
91
- // The three declarations as source, for the generated code to carry. Derived
105
+ // The four declarations as source, for the generated code to carry. Derived
92
106
  // from the functions above rather than written out again, so a fix to one
93
107
  // cannot miss the other. `tests/test_branch_collapse.js` runs both.
94
108
  function embedSource () {
95
109
  return 'const __ATA_SEVERITY=' + JSON.stringify(__ATA_SEVERITY) + ';' +
96
110
  'const __ataScore=' + __ataScore.toString() + ';' +
111
+ 'const __ataMulti=' + __ataMulti.toString() + ';' +
97
112
  'const __ataCollapse=' + __ataCollapse.toString() + ';';
98
113
  }
99
114
 
@@ -185,40 +185,56 @@ function rankFor (keyword) {
185
185
  function enrich (rawErr, opts) {
186
186
  const data = opts && opts.data;
187
187
  const positions = opts && opts.positions;
188
- const format = rawErr.params && rawErr.params.format;
189
- // The native engine reports its own enum as a number. Translate it to the
190
- // keyword and public code the JavaScript engines use, so an error means the
191
- // same thing whichever engine produced it.
192
- const fromAddon = typeof rawErr.code === 'number' ? fromNative(rawErr.code, format) : null;
193
- const keyword = rawErr.keyword || (fromAddon && fromAddon.keyword);
194
- // Prefer a code the codegen already attached (e.g. branch-collapse emits
195
- // ATA4001/4002/4003 distinguishing zero/multi/anyOf failure modes). The
196
- // keyword-derived lookup only finds the first match for `keyword: 'oneOf'`.
197
- // A property that dependentRequired asks for is reported with the keyword
198
- // `required`, and its documented code is ATA7005, not required's ATA7001.
199
- // The error generator stamped 7005 and the other engines left it to the
200
- // keyword here, so the same failure carried either code depending on which
201
- // generator answered.
202
- const dependent = keyword === 'required' && typeof rawErr.schemaPath === 'string' && rawErr.schemaPath.endsWith('/dependentRequired');
203
- const code = (fromAddon && fromAddon.code) ||
204
- (typeof rawErr.code === 'string' && rawErr.code) ||
205
- (dependent && 'ATA7005') ||
206
- codeFor(keyword, format) ||
207
- 'ATA9001';
208
- const meta = CODES[code];
209
- const path = rawErr.instancePath != null ? rawErr.instancePath : (rawErr.path || '');
210
-
188
+ // A literal the generated code shares between calls carries a slot for the
189
+ // fields that depend on nothing but the literal: worked out on the first
190
+ // read, taken from the slot after that.
191
+ let st = rawErr._t;
192
+ if (st === undefined || st.code === undefined) {
193
+ const format = rawErr.params && rawErr.params.format;
194
+ // The native engine reports its own enum as a number. Translate it to the
195
+ // keyword and public code the JavaScript engines use, so an error means the
196
+ // same thing whichever engine produced it.
197
+ const fromAddon = typeof rawErr.code === 'number' ? fromNative(rawErr.code, format) : null;
198
+ const keyword = rawErr.keyword || (fromAddon && fromAddon.keyword);
199
+ // Prefer a code the codegen already attached (e.g. branch-collapse emits
200
+ // ATA4001/4002/4003 distinguishing zero/multi/anyOf failure modes). The
201
+ // keyword-derived lookup only finds the first match for `keyword: 'oneOf'`.
202
+ // A property that dependentRequired asks for is reported with the keyword
203
+ // `required`, and its documented code is ATA7005, not required's ATA7001.
204
+ // The error generator stamped 7005 and the other engines left it to the
205
+ // keyword here, so the same failure carried either code depending on which
206
+ // generator answered.
207
+ const dependent = keyword === 'required' && typeof rawErr.schemaPath === 'string' && rawErr.schemaPath.endsWith('/dependentRequired');
208
+ const code = (fromAddon && fromAddon.code) ||
209
+ (typeof rawErr.code === 'string' && rawErr.code) ||
210
+ (dependent && 'ATA7005') ||
211
+ codeFor(keyword, format) ||
212
+ 'ATA9001';
213
+ const meta = CODES[code];
214
+ const fixed = {
215
+ code,
216
+ message: rawErr.message || (meta && meta.headline) || 'validation failed',
217
+ keyword,
218
+ path: rawErr.instancePath != null ? rawErr.instancePath : (rawErr.path || ''),
219
+ expected: expectedFor(rawErr),
220
+ docUrl: DOC_BASE + code,
221
+ rank: rankFor(keyword),
222
+ };
223
+ if (st !== undefined && !fromAddon) Object.assign(st, fixed);
224
+ st = fixed;
225
+ }
226
+ const path = st.path;
211
227
  const at = data !== undefined ? resolveAt(rawErr, data) : MISSING;
212
228
 
213
229
  const out = {
214
- code,
215
- message: rawErr.message || (meta && meta.headline) || 'validation failed',
216
- keyword,
230
+ code: st.code,
231
+ message: st.message,
232
+ keyword: st.keyword,
217
233
  path,
218
- expected: expectedFor(rawErr),
234
+ expected: st.expected,
219
235
  received: at === MISSING ? undefined : reprValue(at),
220
236
  schemaPath: rawErr.schemaPath,
221
- docUrl: DOC_BASE + code,
237
+ docUrl: st.docUrl,
222
238
  // Back-compat aliases (additive, present in both rich and legacy paths)
223
239
  instancePath: path,
224
240
  dataPath: path,
@@ -264,7 +280,7 @@ function enrich (rawErr, opts) {
264
280
 
265
281
  const detail = detailFor(rawErr, out);
266
282
  if (detail !== undefined) out.detail = detail;
267
- out.rank = rankFor(keyword);
283
+ out.rank = st.rank;
268
284
 
269
285
  // Token-level anchor. `dataFrame` already carries the value span; `anchor`
270
286
  // adds the key span so a caret can sit on the property that is wrong rather
@@ -287,4 +303,4 @@ function enrich (rawErr, opts) {
287
303
  return out;
288
304
  }
289
305
 
290
- module.exports = { enrich, reprValue, expectedFor };
306
+ module.exports = { enrich, reprValue, expectedFor, detailFor, rankFor, MISSING, DOC_BASE };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { CODES, codeFor } = require('./error-codes');
4
+ const { suggestFor } = require('./suggestions');
5
+ const { reprValue, expectedFor, detailFor, rankFor, MISSING, DOC_BASE } = require('./enrich-error');
6
+
7
+ // Enrichment done where the error happens. The combined generator, once a
8
+ // validator's errors are being read, calls `makeRich` at each failure with a
9
+ // site record built at compile time by `errorSite` and the value it was
10
+ // checking, and gets the same object `enrich` would build from the raw error:
11
+ // same keys, same order, same values. What depends only on the site (code,
12
+ // docUrl, rank, and `expected` when the params are fixed) is worked out once,
13
+ // and the value comes from the generated code instead of a pointer walk. It is
14
+ // for validate(data) without positions or a schema source; those reads keep
15
+ // `enrich`. tests/test_rich_errors_generated.js holds the two to the same
16
+ // output. A module of its own because only the code generator reaches it, and
17
+ // ata-validator/lite, which enriches through `enrich`, should not carry it.
18
+ const NO_EXPECTED = {};
19
+ function errorSite (keyword, schemaPath, params, format) {
20
+ const dependent = keyword === 'required' && typeof schemaPath === 'string' && schemaPath.endsWith('/dependentRequired');
21
+ const code = (dependent && 'ATA7005') || codeFor(keyword, format) || 'ATA9001';
22
+ return {
23
+ code,
24
+ keyword,
25
+ schemaPath,
26
+ docUrl: DOC_BASE + code,
27
+ rank: rankFor(keyword),
28
+ headline: (CODES[code] && CODES[code].headline) || 'validation failed',
29
+ expected: params === undefined ? NO_EXPECTED : expectedFor({ keyword, params }),
30
+ };
31
+ }
32
+
33
+ function makeRich (site, path, params, message, value, root, spPrefix) {
34
+ // As resolveAt: a root that is null, undefined, '' or NaN resolves nothing.
35
+ const at = (!root && root !== 0 && root !== false) ? MISSING : value;
36
+ const out = {
37
+ code: site.code,
38
+ message: message || site.headline,
39
+ keyword: site.keyword,
40
+ path,
41
+ expected: site.expected === NO_EXPECTED ? expectedFor({ keyword: site.keyword, params }) : site.expected,
42
+ received: at === MISSING ? undefined : reprValue(at),
43
+ schemaPath: spPrefix === undefined ? site.schemaPath : spPrefix + site.schemaPath,
44
+ docUrl: site.docUrl,
45
+ instancePath: path,
46
+ dataPath: path,
47
+ params,
48
+ parentSchema: undefined,
49
+ };
50
+ const sugg = suggestFor(out, root, at === MISSING ? undefined : at);
51
+ if (sugg) out.suggestion = sugg;
52
+ const detail = detailFor({ keyword: site.keyword, params }, out);
53
+ if (detail !== undefined) out.detail = detail;
54
+ out.rank = site.rank;
55
+ return out;
56
+ }
57
+
58
+ module.exports = { errorSite, makeRich };
@@ -1255,6 +1255,12 @@ function createInterpreter(schema, options) {
1255
1255
  }
1256
1256
 
1257
1257
  module.exports = { createInterpreter };
1258
+ // The resolver, for the code generator: it resolves references at compile
1259
+ // time the way this engine does at run time (normalizeRefs in js-compiler).
1260
+ Object.defineProperty(module.exports, '_refInternals', {
1261
+ value: { indexSchemas, resolveRef, FALLBACK_BASE },
1262
+ enumerable: false,
1263
+ });
1258
1264
  // For lib/plan-source.js, which emits source from the same plans and must call
1259
1265
  // the same helpers so the two cannot answer differently.
1260
1266
  Object.defineProperty(module.exports, '_planInternals', {