ata-validator 1.34.0 → 1.36.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.
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ // The rejection objects validate() and validateJSON() return, and the helpers
4
+ // that build their errors on first read: declaration-order sorting,
5
+ // enrichment, typo correlation and source frames. Shared by the validator core
6
+ // and by the wrapper around a compiled module, so both report errors through
7
+ // the same code.
8
+
9
+ const { ordinalFor: schemaOrdinal, rankFor: schemaRank } = require('./schema-order');
10
+ const { setDiagnosticSource: attachDiagnosticSource } = require('./diagnostic-source');
11
+
12
+ // Rejection result with errors materialized on first read. The accessor
13
+ // lives on the prototype so constructing one is a plain allocation; an
14
+ // object-literal getter would create a closure and define an accessor
15
+ // property on every rejection, which showed up as the single largest cost
16
+ // on the rejection path. `toJSON` keeps JSON.stringify output identical to
17
+ // the eager shape. Note for tests: deepStrictEqual against a plain object
18
+ // compares prototypes; read `.errors` and compare that.
19
+ class LazyRejection {
20
+ constructor(build, data, buildRaw) {
21
+ this.valid = false;
22
+ this._build = build;
23
+ this._data = data;
24
+ this._errors = null;
25
+ this._buildRaw = buildRaw;
26
+ }
27
+ toJSON() {
28
+ return { valid: false, errors: this.errors };
29
+ }
30
+ // Raw shape for consumers that carry only message and path, such as the
31
+ // Standard Schema bridge: schema order, no enrichment. Reading `errors`
32
+ // afterwards still enriches through its own build.
33
+ _ataRaw() {
34
+ return this._buildRaw ? this._buildRaw(this._data) : this.errors;
35
+ }
36
+ }
37
+ Object.defineProperty(LazyRejection.prototype, 'errors', {
38
+ enumerable: true,
39
+ configurable: true,
40
+ get() {
41
+ if (this._errors === null) this._errors = this._build(this._data);
42
+ return this._errors;
43
+ },
44
+ });
45
+
46
+ let _enrichFn = null;
47
+ function _enrichLazy(e, opts) {
48
+ if (_enrichFn === null) _enrichFn = require('./enrich-error').enrich;
49
+ return _enrichFn(e, opts);
50
+ }
51
+
52
+ // The rejection the rich-errors wrapper returns. `errors` is built on first
53
+ // read and cached; `_ataRaw()` is the raw, schema-ordered list for consumers
54
+ // that carry only message and path, such as the Standard Schema bridge.
55
+ // Prototype accessors, not per-instance ones: see LazyRejection.
56
+ class RichRejection {
57
+ // `rawInput` is the JSON text validateJSON was given, or null for validate(data).
58
+ // The position map it implies is built in the `errors` getter, not here: it is
59
+ // a full walk of the document, it is only ever read through an error's
60
+ // dataFrame, and building it on every rejection cost a caller that reads
61
+ // `.valid` about 460 microseconds on a 50 KB document. Holding the text rather
62
+ // than reading `self._lastRawInput` later also keeps the result independent of
63
+ // what the instance does after this call returns.
64
+ constructor(result, data, rawInput, self, root, enrich) {
65
+ this.valid = false;
66
+ this._result = result;
67
+ this._data = data;
68
+ this._rawInput = rawInput;
69
+ this._self = self;
70
+ this._root = root;
71
+ this._enrich = enrich;
72
+ this._cached = null;
73
+ }
74
+ toJSON() {
75
+ return { valid: false, errors: this.errors };
76
+ }
77
+ _ataRaw() {
78
+ let raw = this._result.errors || [];
79
+ if (raw.length > 1) raw = sortErrorsBySchemaOrder(this._root, raw);
80
+ return raw;
81
+ }
82
+ }
83
+ Object.defineProperty(RichRejection.prototype, 'errors', {
84
+ enumerable: true,
85
+ configurable: true,
86
+ get() {
87
+ 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;
121
+ }
122
+ return this._cached;
123
+ },
124
+ });
125
+
126
+ // The rejection validateJSON returns. Everything the text path adds over
127
+ // validate(data), the value tree enrichment needs, the position map, the
128
+ // diagnostic payload, happens on first access to `.errors`. Reading `.valid`
129
+ // touches none of it. The JSON text is held here rather than read back off the
130
+ // validator, so the result does not depend on what the instance does next.
131
+ class LazyJsonRejection {
132
+ constructor(result, jsonStr, self, enrich) {
133
+ this.valid = false;
134
+ this._result = result;
135
+ this._jsonStr = jsonStr;
136
+ this._self = self;
137
+ this._enrich = enrich;
138
+ this._cached = null;
139
+ }
140
+ toJSON() {
141
+ return { valid: false, errors: this.errors };
142
+ }
143
+ }
144
+ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
145
+ enumerable: true,
146
+ configurable: true,
147
+ get() {
148
+ if (this._cached !== null) return this._cached;
149
+ const self = this._self;
150
+ const enrich = this._enrich;
151
+ const jsonStr = this._jsonStr;
152
+ // Reading the inner errors realizes the inner lazy layer, if there was one.
153
+ const raw = this._result.errors || [];
154
+ if (!raw.length) { this._cached = raw; return raw; }
155
+
156
+ // The enrich pass plucks `received` from the value tree and the suggestion
157
+ // engine (required-typo, format hints, coercion nudges) walks it too.
158
+ let parsedData;
159
+ try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
160
+ // What was checked is the document after defaults, coercion and removal,
161
+ // so `received` has to come from that, as it does for validate(). A
162
+ // default that failed its own schema has no value in the text at all.
163
+ if (parsedData !== undefined && self._preprocess) self._preprocess(parsedData);
164
+
165
+ // Errors the inner path already enriched carry a `received` key: enrich()
166
+ // always sets one, even when there is no value to show. `code` is not a
167
+ // safe signal, because branch-collapse attaches codes to raw errors, and
168
+ // `docUrl` is not either, because the generated error functions stamp it
169
+ // on raw errors; taking it as the signal left those errors without
170
+ // `received` or a suggestion on the text path while validate() had both.
171
+ if (!raw[0] || !('received' in raw[0])) {
172
+ const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
173
+ // Declaration order, as validate() applies it; the text path used to
174
+ // enrich in emission order.
175
+ const ordered = raw.length > 1 ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
176
+ const enrichOpts = {
177
+ data: parsedData,
178
+ positions,
179
+ schemaPositions: self._schemaPositions,
180
+ schemaFile: self._source ? self._source.path : undefined,
181
+ };
182
+ const enriched = ordered.map((e) => enrich(e, enrichOpts));
183
+ if (enriched.length > 1) attachRelated(enriched);
184
+ attachDiagnosticSource(enriched, {
185
+ data: parsedData,
186
+ text: jsonStr,
187
+ positions,
188
+ schema: self._schemaObj,
189
+ mutatesInput: self._mutatesInput === true,
190
+ });
191
+ if (positions) self._posCache.reset();
192
+ this._cached = enriched;
193
+ return enriched;
194
+ }
195
+
196
+ // Already enriched, so the frames are usually attached too. Only a gap in
197
+ // them is worth another walk of the document: resolving the map to discover
198
+ // there was nothing to fill cost a second full walk on every rejection.
199
+ if (raw.some((e) => e && !e.dataFrame)) {
200
+ const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw.filter((e) => e && !e.dataFrame)));
201
+ if (positions) {
202
+ for (const e of raw) {
203
+ if (e && !e.dataFrame) {
204
+ const path = e.path != null ? e.path : (e.instancePath || '');
205
+ const p = positions[path];
206
+ if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
207
+ }
208
+ }
209
+ self._posCache.reset();
210
+ }
211
+ }
212
+ if (raw.length > 1) attachRelated(raw);
213
+ attachDiagnosticSource(raw, {
214
+ data: parsedData,
215
+ text: jsonStr,
216
+ schema: self._schemaObj,
217
+ mutatesInput: self._mutatesInput === true,
218
+ });
219
+ this._cached = raw;
220
+ return raw;
221
+ },
222
+ });
223
+
224
+ // The pointers a set of errors will ask the position map about. lib/enrich-error
225
+ // looks up the error's own path, and for an additional or unevaluated property
226
+ // the child pointer named in `params`, unescaped, which is the form it asks for.
227
+ // The escaped form goes in too: including a pointer that is never read costs
228
+ // nothing, and the cache answers anything outside this set from the full map
229
+ // rather than reporting no position.
230
+ function wantedPointersFor (errors) {
231
+ const wanted = new Set();
232
+ for (const e of errors) {
233
+ if (!e) continue;
234
+ const path = e.path != null ? e.path : (e.instancePath || '');
235
+ wanted.add(path);
236
+ if (e.instancePath != null && e.instancePath !== path) wanted.add(e.instancePath);
237
+ const params = e.params;
238
+ const named = params && (params.additionalProperty || params.unevaluatedProperty);
239
+ if (typeof named === 'string') {
240
+ wanted.add(path + '/' + named);
241
+ if (named.indexOf('~') !== -1 || named.indexOf('/') !== -1) {
242
+ wanted.add(path + '/' + named.replace(/~/g, '~0').replace(/\//g, '~1'));
243
+ }
244
+ }
245
+ }
246
+ return wanted;
247
+ }
248
+
249
+ // A raw error without the `_o` ordering key, for the legacy error shape.
250
+ function stripOrdinal(e) {
251
+ if (e === null || typeof e !== 'object' || e._o === undefined) return e;
252
+ const out = {};
253
+ for (const k in e) if (k !== '_o') out[k] = e[k];
254
+ return out;
255
+ }
256
+
257
+ // Errors in schema declaration order. Each error's key is its schemaPath's
258
+ // pre-order ordinal in the root schema: written into the literal by the code
259
+ // generator (`_o`), looked up once per path otherwise. Errors behind the same
260
+ // `$ref` share the ordinal of that `$ref`, and among themselves go by their
261
+ // place in the target (rankFor), so they come out in the order they would
262
+ // inline, whichever engine produced them. Most rejections come out already
263
+ // ordered, and those return without sorting or allocating.
264
+ function sortErrorsBySchemaOrder(rootSchema, errors) {
265
+ const n = errors.length;
266
+ const keys = new Array(n);
267
+ let sorted = true;
268
+ let prev = -1;
269
+ let prevPath = null;
270
+ for (let i = 0; i < n; i++) {
271
+ const e = errors[i];
272
+ let o = typeof e._o === 'number' ? e._o : schemaOrdinal(rootSchema, e.schemaPath);
273
+ // An error with no place in this document (an appended custom-keyword
274
+ // error, a path into another schema) stays next to the error before it,
275
+ // which is where the rank comparison left it too.
276
+ if (o === null) o = prev < 0 ? 0 : prev;
277
+ keys[i] = o;
278
+ if (o < prev || (o === prev && sorted && e.schemaPath !== prevPath && tieOrder(rootSchema, prevPath, e.schemaPath) > 0)) sorted = false;
279
+ prev = o;
280
+ prevPath = e.schemaPath;
281
+ }
282
+ if (sorted) return errors;
283
+ // Error lists are short. A stable insertion sort over the integer keys
284
+ // moves the errors in tandem with no index array; equal keys fall back to
285
+ // the rank only when the paths differ.
286
+ const out = errors.slice();
287
+ for (let i = 1; i < n; i++) {
288
+ const k = keys[i];
289
+ const e = out[i];
290
+ let j = i - 1;
291
+ while (j >= 0 && (keys[j] > k || (keys[j] === k && tieOrder(rootSchema, out[j].schemaPath, e.schemaPath) > 0))) {
292
+ keys[j + 1] = keys[j]; out[j + 1] = out[j]; j--;
293
+ }
294
+ keys[j + 1] = k;
295
+ out[j + 1] = e;
296
+ }
297
+ return out;
298
+ }
299
+
300
+ // Order of two paths with the same ordinal: positive when `a` belongs after
301
+ // `b`. Zero keeps them as they are, for identical paths and for paths
302
+ // outside this document.
303
+ function tieOrder(rootSchema, a, b) {
304
+ if (a === b) return 0;
305
+ const ra = schemaRank(rootSchema, a), rb = schemaRank(rootSchema, b);
306
+ if (ra === null || rb === null) return 0;
307
+ const m = Math.min(ra.length, rb.length);
308
+ for (let k = 0; k < m; k++) if (ra[k] !== rb[k]) return ra[k] - rb[k];
309
+ return ra.length - rb.length;
310
+ }
311
+
312
+ // Resolved once. A require() inside the function was re-resolving the path
313
+ // on every rejection, which the profile showed as internalModuleStat at the
314
+ // top of the reject path, above the correlation it was loading.
315
+ let _correlateTypos = null;
316
+ function attachRelated (errors) {
317
+ if (_correlateTypos === null) _correlateTypos = require('./correlate').correlateTypos;
318
+ const pairs = _correlateTypos(errors);
319
+ if (pairs === null) return errors;
320
+ for (const [from, to] of pairs) {
321
+ const e = errors[from];
322
+ if (!e) continue;
323
+ if (e.related) { if (!e.related.includes(to)) e.related.push(to); }
324
+ else e.related = [to];
325
+ }
326
+ return errors;
327
+ }
328
+
329
+ module.exports = { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, attachRelated, sortErrorsBySchemaOrder, stripOrdinal, wantedPointersFor };
@@ -11,6 +11,11 @@
11
11
  // generator can compute it once per error site instead of the reader
12
12
  // deriving it from the path string on every rejection.
13
13
  //
14
+ // A path that runs on through a `$ref` names keys the referencing node does
15
+ // not have. Its ordinal is that of the `$ref` key, which several paths then
16
+ // share; its rank carries on inside the target, and the reader uses the rank
17
+ // to order the paths that share an ordinal.
18
+ //
14
19
  // Both are memoized per root schema; a validator's root never changes.
15
20
 
16
21
  const _keyIndexCache = new WeakMap();
@@ -56,12 +61,12 @@ function computeRank(rootSchema, schemaPath) {
56
61
  const rank = [];
57
62
  let node = rootSchema;
58
63
  let start = 1;
64
+ let hops = 0;
59
65
  while (start <= schemaPath.length) {
60
66
  let end = schemaPath.indexOf('/', start);
61
67
  if (end < 0) end = schemaPath.length;
62
68
  if (end === start) { start = end + 1; continue; }
63
69
  const seg = unescapePointerSegment(schemaPath.slice(start, end));
64
- start = end + 1;
65
70
  if (node == null || typeof node !== 'object') break;
66
71
  if (Array.isArray(node)) {
67
72
  const idx = Number(seg);
@@ -70,14 +75,53 @@ function computeRank(rootSchema, schemaPath) {
70
75
  node = node[idx];
71
76
  } else {
72
77
  const idx = keyIndex(node, seg);
73
- if (idx < 0) break;
78
+ if (idx < 0) {
79
+ // An evaluation path runs on through a `$ref` into its target without
80
+ // naming it. Ranking the rest inside the target, as if the target sat
81
+ // under the `$ref` key, orders errors behind a reference by the
82
+ // target's declaration order, as they are ordered inline.
83
+ const target = hops < 64 ? localTarget(rootSchema, node) : null;
84
+ if (target === null) break;
85
+ hops++;
86
+ rank.push(keyIndex(node, '$ref'));
87
+ node = target;
88
+ continue;
89
+ }
74
90
  rank.push(idx);
75
91
  node = node[seg];
76
92
  }
93
+ start = end + 1;
77
94
  }
78
95
  return rank;
79
96
  }
80
97
 
98
+ // The schema a node's `$ref` names, when it is a pointer into this document.
99
+ function localTarget(rootSchema, node) {
100
+ const ref = node.$ref;
101
+ if (typeof ref !== 'string' || ref.charCodeAt(0) !== 35) return null;
102
+ if (ref.length > 1 && ref.charCodeAt(1) !== 47) return null;
103
+ let t = rootSchema;
104
+ let start = 2;
105
+ while (start <= ref.length && ref.length > 1) {
106
+ let end = ref.indexOf('/', start);
107
+ if (end < 0) end = ref.length;
108
+ let seg = ref.slice(start, end);
109
+ if (seg.indexOf('%') >= 0) { try { seg = decodeURIComponent(seg); } catch { return null; } }
110
+ seg = unescapePointerSegment(seg);
111
+ if (t === null || typeof t !== 'object') return null;
112
+ // Normalization renames draft-07 `definitions` to `$defs` and leaves the
113
+ // pointers as written; the resolvers read either name as the other.
114
+ if (!Object.prototype.hasOwnProperty.call(t, seg)) {
115
+ const alias = seg === 'definitions' ? '$defs' : seg === '$defs' ? 'definitions' : null;
116
+ if (alias === null || !Object.prototype.hasOwnProperty.call(t, alias)) return null;
117
+ seg = alias;
118
+ }
119
+ t = t[seg];
120
+ start = end + 1;
121
+ }
122
+ return t !== null && typeof t === 'object' && t !== node ? t : null;
123
+ }
124
+
81
125
  // Pre-order numbering of every node in the schema, keyed by its pointer as
82
126
  // it appears in a schemaPath (segments escaped). Built once per root, on the
83
127
  // first ask. Cyclic structures are guarded; a schema object can contain
@@ -115,13 +159,19 @@ function ordinalFor(rootSchema, schemaPath) {
115
159
  // Walk back to the longest prefix that exists. Paths through a keyword's
116
160
  // value that is a primitive, or through a key the schema does not have,
117
161
  // land on the nearest enclosing node.
162
+ // A path that runs on through a `$ref` lands on its `$ref` key, so errors
163
+ // behind a reference sort where the `$ref` is declared among its siblings;
164
+ // rankFor orders them among themselves.
118
165
  let p = schemaPath;
119
166
  while (true) {
120
167
  const cut = p.lastIndexOf('/');
121
168
  if (cut < 0) return 0;
122
169
  p = p.slice(0, cut);
123
170
  const o = map.get(p);
124
- if (o !== undefined) return o;
171
+ if (o !== undefined) {
172
+ const r = map.get(p + '/$ref');
173
+ return r !== undefined ? r : o;
174
+ }
125
175
  }
126
176
  }
127
177