ata-validator 1.33.3 → 1.35.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,309 @@
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 } = 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. Most rejections come
260
+ // out already ordered, and those return without sorting or allocating.
261
+ function sortErrorsBySchemaOrder(rootSchema, errors) {
262
+ const n = errors.length;
263
+ const keys = new Array(n);
264
+ let sorted = true;
265
+ let prev = -1;
266
+ for (let i = 0; i < n; i++) {
267
+ const e = errors[i];
268
+ let o = typeof e._o === 'number' ? e._o : schemaOrdinal(rootSchema, e.schemaPath);
269
+ // An error with no place in this document (an appended custom-keyword
270
+ // error, a path into another schema) stays next to the error before it,
271
+ // which is where the rank comparison left it too.
272
+ if (o === null) o = prev < 0 ? 0 : prev;
273
+ keys[i] = o;
274
+ if (o < prev) sorted = false;
275
+ prev = o;
276
+ }
277
+ if (sorted) return errors;
278
+ // Error lists are short. A stable insertion sort over the integer keys
279
+ // moves the errors in tandem with no comparator calls and no index array.
280
+ const out = errors.slice();
281
+ for (let i = 1; i < n; i++) {
282
+ const k = keys[i];
283
+ const e = out[i];
284
+ let j = i - 1;
285
+ while (j >= 0 && keys[j] > k) { keys[j + 1] = keys[j]; out[j + 1] = out[j]; j--; }
286
+ keys[j + 1] = k;
287
+ out[j + 1] = e;
288
+ }
289
+ return out;
290
+ }
291
+
292
+ // Resolved once. A require() inside the function was re-resolving the path
293
+ // on every rejection, which the profile showed as internalModuleStat at the
294
+ // top of the reject path, above the correlation it was loading.
295
+ let _correlateTypos = null;
296
+ function attachRelated (errors) {
297
+ if (_correlateTypos === null) _correlateTypos = require('./correlate').correlateTypos;
298
+ const pairs = _correlateTypos(errors);
299
+ if (pairs === null) return errors;
300
+ for (const [from, to] of pairs) {
301
+ const e = errors[from];
302
+ if (!e) continue;
303
+ if (e.related) { if (!e.related.includes(to)) e.related.push(to); }
304
+ else e.related = [to];
305
+ }
306
+ return errors;
307
+ }
308
+
309
+ module.exports = { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, attachRelated, sortErrorsBySchemaOrder, stripOrdinal, wantedPointersFor };