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/lib/rejections.js CHANGED
@@ -76,53 +76,57 @@ class RichRejection {
76
76
  }
77
77
  _ataRaw() {
78
78
  let raw = this._result.errors || [];
79
- if (raw.length > 1) raw = sortErrorsBySchemaOrder(this._root, raw);
79
+ if (needsOrdering(raw)) raw = sortErrorsBySchemaOrder(this._root, raw);
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 (raw.length > 1) 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`
@@ -172,7 +176,7 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
172
176
  const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
173
177
  // Declaration order, as validate() applies it; the text path used to
174
178
  // enrich in emission order.
175
- const ordered = raw.length > 1 ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
179
+ const ordered = needsOrdering(raw) ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
176
180
  const enrichOpts = {
177
181
  data: parsedData,
178
182
  positions,
@@ -246,11 +250,26 @@ function wantedPointersFor (errors) {
246
250
  return wanted;
247
251
  }
248
252
 
249
- // A raw error without the `_o` ordering key, for the legacy error shape.
253
+ // A raw error in the legacy shape. The error generator writes `code` and
254
+ // `docUrl` into its literals because a standalone module returns them as they
255
+ // are; the runtime's legacy shape never had them, and the interpreter leaves
256
+ // them out, so they are dropped here with the ordering key `_o`. A collapsed
257
+ // oneOf/anyOf error keeps its code, which both engines give it, and its
258
+ // branch errors get the same treatment, so the shape does not depend on
259
+ // which engine answered.
260
+ const COLLAPSE_CODES = new Set(['ATA4001', 'ATA4002', 'ATA4003']);
250
261
  function stripOrdinal(e) {
251
- if (e === null || typeof e !== 'object' || e._o === undefined) return e;
262
+ if (e === null || typeof e !== 'object') 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;
252
267
  const out = {};
253
- for (const k in e) if (k !== '_o') out[k] = e[k];
268
+ for (const k in e) {
269
+ if (k === '_o' || k === 'docUrl') continue;
270
+ if (k === 'code' && !COLLAPSE_CODES.has(e.code)) continue;
271
+ out[k] = k === 'branchErrors' && Array.isArray(e[k]) ? e[k].map(stripOrdinal) : e[k];
272
+ }
254
273
  return out;
255
274
  }
256
275
 
@@ -261,7 +280,33 @@ function stripOrdinal(e) {
261
280
  // place in the target (rankFor), so they come out in the order they would
262
281
  // inline, whichever engine produced them. Most rejections come out already
263
282
  // ordered, and those return without sorting or allocating.
283
+ // A list of one can still hold a collapsed error whose branch errors, at any
284
+ // depth, need ordering.
285
+ function needsOrdering(raw) {
286
+ if (raw.length > 1) return true;
287
+ const e = raw[0];
288
+ return raw.length === 1 && e !== null && typeof e === 'object' && Array.isArray(e.branchErrors) && needsOrdering(e.branchErrors);
289
+ }
290
+
291
+ // A collapsed oneOf/anyOf error carries the closest branch's errors, which
292
+ // each engine produces in its own walk order. They go in schema order like the
293
+ // list they sit in, so which engine answered cannot be read off them.
294
+ function sortBranchErrors(rootSchema, errors) {
295
+ let out = null;
296
+ for (let i = 0; i < errors.length; i++) {
297
+ const e = errors[i];
298
+ const b = e !== null && typeof e === 'object' ? e.branchErrors : undefined;
299
+ if (!Array.isArray(b) || b.length === 0) continue;
300
+ const sb = sortErrorsBySchemaOrder(rootSchema, b);
301
+ if (sb === b) continue;
302
+ if (out === null) out = errors.slice();
303
+ out[i] = Object.assign({}, e, { branchErrors: sb });
304
+ }
305
+ return out === null ? errors : out;
306
+ }
307
+
264
308
  function sortErrorsBySchemaOrder(rootSchema, errors) {
309
+ errors = sortBranchErrors(rootSchema, errors);
265
310
  const n = errors.length;
266
311
  const keys = new Array(n);
267
312
  let sorted = true;
@@ -326,4 +371,4 @@ function attachRelated (errors) {
326
371
  return errors;
327
372
  }
328
373
 
329
- 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
  }