ata-validator 1.49.0 → 1.51.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,1024 @@
1
+ 'use strict';
2
+ const { compileFunction } = require('./compile-fn')
3
+
4
+ // The code generator as the validator core takes it (core._registerCodegen):
5
+ // the compiler and the paths a Validator installs when it produced a verdict
6
+ // function. index.js registers it; the edge entry (index.edge.js), for
7
+ // runtimes where generated code cannot run, and ata-validator/lite do not,
8
+ // so their bundles leave all of it out.
9
+ const core = require('./validator-core.js');
10
+
11
+ const jsCompiler = require('./js-compiler.js');
12
+ const { compileToJS, compileToJSCodegen } = jsCompiler;
13
+
14
+ // The compiled paths a Validator installs when the code generator produced a
15
+ // verdict function: the hybrid and combined forms of validate(), the lazy
16
+ // error resolvers, validateJSON() over simdjson and the scanner, abortEarly.
17
+ // They live here rather than in Validator#_ensureCompiled so that
18
+ // ata-validator/lite, which never has a verdict function, does not carry
19
+ // them. Called with the validator as `this` and the locals of
20
+ // _ensureCompiled in `ctx`; the error and combined functions are built later
21
+ // by the builders, so they are read through ctx.err() and ctx.combined().
22
+
23
+ // A rejection the native walker decided from the text alone. The errors come
24
+ // from parsing and validating in JS, on first read, so a caller that reads
25
+ // only `.valid` never parses the document.
26
+ class TextRejection {
27
+ constructor (text, validateText) {
28
+ this.valid = false;
29
+ this._text = text;
30
+ this._validateText = validateText;
31
+ this._errors = null;
32
+ }
33
+
34
+ get errors () {
35
+ if (this._errors === null) this._errors = core._internals._mustReject(this._validateText(this._text)).errors;
36
+ return this._errors;
37
+ }
38
+ }
39
+
40
+ // Errors built enriched where they fail by the rich combined function (see
41
+ // makeRich in lib/enrich-site.js), for a validator whose errors are being read
42
+ // with richErrors on. Presenting them is the ordering and the `related` links
43
+ // presentErrors applies after enriching. Where the rich function does not
44
+ // reject (the data changed under the read), the plain fallback is presented
45
+ // the usual way.
46
+ const { needsOrdering: _needsOrdering, sortErrorsBySchemaOrder: _sortErrors, attachRelated: _attachRelated } = require('./rejections');
47
+ let _enrichFnIdx = null;
48
+ function _enrichOne(e, data) {
49
+ if (_enrichFnIdx === null) _enrichFnIdx = require('./enrich-error').enrich;
50
+ return _enrichFnIdx(e, data !== undefined ? { data } : null);
51
+ }
52
+ function richBuilder(combRich, root, presentFallback, fallback, plain, cleanPlain) {
53
+ if (plain) {
54
+ // The runtime-shape combined function writes each error fresh and in the
55
+ // plain shape: no shared literal, no ordinal, no code or docUrl, no
56
+ // collapsed branches (lib/js-compiler.js, compileToJSCombined with
57
+ // runtimeShape). So without enrichment the list is the caller's as it is,
58
+ // once in schema order. Checking each error for those fields instead cost
59
+ // a megamorphic load per field per error across many schemas.
60
+ // tests/test_error_read_paths.js holds every error handed out to being
61
+ // the caller's own.
62
+ return (data) => {
63
+ const r = combRich(data);
64
+ if (r.valid || !r.errors || !r.errors.length) return presentFallback(fallback, data);
65
+ if (cleanPlain) return _needsOrdering(r.errors) ? _sortErrors(root, r.errors) : r.errors;
66
+ return presentFallback(r.errors, data);
67
+ };
68
+ }
69
+ return (data) => {
70
+ const r = combRich(data);
71
+ if (r.valid || !r.errors || !r.errors.length) return presentFallback(fallback, data);
72
+ const out = _needsOrdering(r.errors) ? _sortErrors(root, r.errors) : r.errors;
73
+ // A collapsed oneOf or anyOf comes out plain (no docUrl) and is enriched
74
+ // here, its branch errors with it, as presentErrors would.
75
+ for (let i = 0; i < out.length; i++) {
76
+ if (out[i].docUrl === undefined) out[i] = _enrichOne(out[i], data);
77
+ }
78
+ if (out.length > 1) _attachRelated(out);
79
+ return out;
80
+ };
81
+ }
82
+
83
+ // The rejection validate() returns once it is the one-pass combined function
84
+ // (resultShape in compileToJSCombined): the errors are already built, plain,
85
+ // fresh and in schema order. Serialises as the lazy rejection does.
86
+ class EagerRejection {
87
+ constructor(errors) {
88
+ this.valid = false;
89
+ this.errors = errors;
90
+ }
91
+ toJSON() {
92
+ return { valid: false, errors: this.errors };
93
+ }
94
+ _ataRaw() {
95
+ return this.errors;
96
+ }
97
+ }
98
+
99
+ function installCodegenPaths (ctx) {
100
+ const { ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindVerdict, _jsonSyntaxRejection, _mustReject, _verboseWrap, getNative, isV1Dialect, resolveSchemaByPath } = core._internals;
101
+ const { jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr } = ctx;
102
+ // errFn: the generated error function when it is safe, else the
103
+ // interpreted engine, on every platform alike.
104
+ const hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
105
+ // The interpreted engine re-validates failing data to produce full errors. If it disagrees with the codegen verdict
106
+ // (it should not), a generic error keeps the result consistent.
107
+ let _interp = null;
108
+ const jsOnlyFallback = (d) => {
109
+ if (jsFn(d)) return { valid: true, data: d, errors: [] };
110
+ if (!_interp) {
111
+ const { createInterpreter } = require('./interpreter');
112
+ _interp = createInterpreter(schemaObj, {
113
+ schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
114
+ formats: this._userFormats,
115
+ v1: isV1Dialect(schemaObj),
116
+ keywords: this._keywords,
117
+ });
118
+ }
119
+ const r = _interp.validate(d);
120
+ if (!r.valid) return r;
121
+ return {
122
+ valid: false,
123
+ errors: [{
124
+ keyword: 'validation',
125
+ instancePath: '',
126
+ schemaPath: '',
127
+ params: {},
128
+ message: 'schema validation failed'
129
+ }]
130
+ };
131
+ };
132
+ // The error generator declines unevaluated*; the interpreted engine
133
+ // reports those schemas correctly, so failing data is re-validated
134
+ // there. This used to be a placeholder error with no keyword and no
135
+ // path, which hid whatever had actually failed.
136
+ // Resolved on the first rejection rather than at compile time, because
137
+ // building the generator behind it is two thirds of what a first call
138
+ // costs and a caller that never reads an error never needs it. The probe
139
+ // moves here with it: it calls the generated function, so it cannot run
140
+ // before the function exists.
141
+ let _errOnlyImpl = null;
142
+ const errOnly = (d) => {
143
+ if (_errOnlyImpl === null) {
144
+ _buildErr();
145
+ let safe = null;
146
+ if (ctx.err()) {
147
+ try {
148
+ ctx.err()({}, true);
149
+ safe = (x) => ctx.err()(x, true);
150
+ } catch {}
151
+ }
152
+ // Where the generator declines, the interpreted engine answers. It used
153
+ // to be the native addon when one was installed, whose errors carry
154
+ // other wording and no schemaPath, so the same validator reported
155
+ // differently depending on what the platform had installed.
156
+ _errOnlyImpl = safe || jsOnlyFallback;
157
+ }
158
+ return _mustReject(_errOnlyImpl(d));
159
+ };
160
+
161
+ // Best path: combined validator (single pass, validates + collects errors)
162
+ // Valid data: returns VALID_RESULT, no allocation
163
+ // Invalid data: collects errors in one pass (no double validation)
164
+ // Fallback: hybridFn or jsFn + errFn for schemas combined can't handle
165
+ // Test combined at compile time -- some schemas produce broken combined code
166
+ // Test combined at compile time -- some schemas (e.g. if/then/else)
167
+ // produce broken combined code that crashes on certain inputs.
168
+ // We probe with diverse data; if any throws, fall back to hybrid.
169
+ let _combinedProbed = false;
170
+ let _safeCombined = null;
171
+ const combinedIfSafe = () => {
172
+ if (_combinedProbed) return _safeCombined;
173
+ _combinedProbed = true;
174
+ _buildCombined();
175
+ if (ctx.combined()) {
176
+ try {
177
+ const probe = {};
178
+ // Populate probe with one key per known property to trigger nested paths
179
+ if (schemaObj && schemaObj.properties) {
180
+ for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
181
+ }
182
+ if (schemaObj && schemaObj.if && schemaObj.if.properties) {
183
+ for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
184
+ }
185
+ ctx.combined()(probe);
186
+ ctx.combined()({});
187
+ ctx.combined()(null);
188
+ ctx.combined()(0);
189
+ _safeCombined = ctx.combined();
190
+ } catch {}
191
+ }
192
+ return _safeCombined;
193
+ };
194
+
195
+ // The error function on its own, probed the way errOnly probes it, or null.
196
+ const errFnIfSafe = () => {
197
+ _buildErr();
198
+ const efn = ctx.err();
199
+ if (!efn) return null;
200
+ try { efn({}, true); } catch { return null; }
201
+ return (x) => efn(x, true);
202
+ };
203
+
204
+ // The rich combined function, built and probed the way the plain one is,
205
+ // and only where the plain one passed its probe.
206
+ let _richProbed = false;
207
+ let _safeRich = null;
208
+ const richIfSafe = () => {
209
+ if (_richProbed) return _safeRich;
210
+ _richProbed = true;
211
+ if (!combinedIfSafe() || !ctx.buildRich) return null;
212
+ const fn = ctx.buildRich();
213
+ if (fn) {
214
+ try {
215
+ const probe = {};
216
+ if (schemaObj && schemaObj.properties) for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
217
+ if (schemaObj && schemaObj.if && schemaObj.if.properties) for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
218
+ fn(probe); fn({}); fn(null); fn(0);
219
+ _safeRich = fn;
220
+ } catch {}
221
+ }
222
+ return _safeRich;
223
+ };
224
+
225
+ // What the hybrid path hands to its error slot: the combined function
226
+ // when it is usable, since it validates and collects in one pass, and
227
+ // the error generator otherwise. Same order the eager code chose, just
228
+ // chosen on the first rejection.
229
+ let _errPreferredImpl = null;
230
+ const errPreferCombined = (d) => {
231
+ if (_errPreferredImpl === null) _errPreferredImpl = combinedIfSafe() || errOnly;
232
+ return _mustReject(_errPreferredImpl(d));
233
+ };
234
+
235
+ // The boolean engine is the verdict authority for these paths; the
236
+ // final lazy wrapper uses it to skip error construction entirely.
237
+ if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
238
+
239
+ if (options.abortEarly && jsFn && !hasDynRef) {
240
+ // abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
241
+ // This is the perf-critical path for edge gateways. The richErrors wrap
242
+ // below recognises the ATA9000 stub keyword and passes the frozen result
243
+ // through unchanged, so a single shared object is returned per failure.
244
+ const _fn = jsFn;
245
+ this.validate = preprocess
246
+ ? (data) => { preprocess(data); return _fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
247
+ : (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
248
+ } else if (hasDynRef && !(_isCodegen && jsFn)) {
249
+ // $dynamicRef without codegen: the interpreted engine. A schema the
250
+ // generators unroll by scope (expandDynamicScopes) takes the assembly
251
+ // below like any other, with the one-pass function once its errors are
252
+ // read; a direct verdict-then-error-function path kept for these from
253
+ // before the unrolling cost the suite's dynamicRef groups 5229 ns a pass
254
+ // where the assembly answers in 3136. (A first attempt dropped them into
255
+ // this interpreter branch instead and measured slower; the condition
256
+ // here is what makes them fall through.) It scores the
257
+ // same on the suite's $dynamicRef cases as the native walker since the
258
+ // dynamic-scope fix, needs no addon, and gets the verdict-only mode.
259
+ if (!_interp) {
260
+ const { createInterpreter } = require('./interpreter');
261
+ _interp = createInterpreter(schemaObj, {
262
+ schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
263
+ formats: this._userFormats,
264
+ v1: isV1Dialect(schemaObj),
265
+ keywords: this._keywords,
266
+ });
267
+ }
268
+ const interp = _interp;
269
+ this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
270
+ this.validate = preprocess
271
+ ? (data) => { preprocess(data); return interp.validate(data); }
272
+ : (data) => interp.validate(data);
273
+ } else if (jsFn && jsFn._hybridFactory) {
274
+ // Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
275
+ // No arrow function wrapper, no ternary, one function call
276
+ // The factory bakes the error function in as an argument and never
277
+ // calls it for a document that passes, so a resolver here costs the
278
+ // accepted path nothing and keeps the compile off the first call.
279
+ // Until the first rejection this is the hybrid: the verdict function,
280
+ // with the error resolver baked in and never called for a document that
281
+ // passes. That keeps the combined function's compile off the first call,
282
+ // which is two thirds of what a first call costs.
283
+ //
284
+ // From the first rejection on, the combined function answers directly.
285
+ // It decides and collects in one pass, so the verdict pass in front of it
286
+ // was validating the document a second time: 134.1 microseconds against
287
+ // its 45.3 on a 1000-user array. Swapping rather than starting there keeps
288
+ // the lazy compile, and swapping at all is only free because the combined
289
+ // function now costs what the verdict function costs on accepted
290
+ // documents (1.02x on that array, 0.99x on a small body) since
291
+ // additionalProperties stopped materialising its key array. The
292
+ // indirection this needs measured inside the noise at both sizes.
293
+ let impl = null;
294
+ const onReject = (data) => {
295
+ const combined = combinedIfSafe();
296
+ if (combined) { impl = combined; return _mustReject(combined(data)); }
297
+ return errOnly(data);
298
+ };
299
+ // Tiered: the first calls go through the verdict function and the
300
+ // resolver, and the hybrid is compiled once the validator is in use.
301
+ // Compiling it up front was a second parse of the whole schema on
302
+ // every validator, most of which answer a handful of requests or only
303
+ // verdicts. A refused compile stays on the first tier.
304
+ let warm = 0;
305
+ const first = (data) => {
306
+ if (++warm >= HYBRID_TIER_CALLS && impl === first) {
307
+ impl = jsFn._hybridFactory(VALID_RESULT, onReject) || ((d) => (jsFn(d) ? VALID_RESULT : onReject(d)));
308
+ return impl(data);
309
+ }
310
+ return jsFn(data) ? VALID_RESULT : onReject(data);
311
+ };
312
+ impl = first;
313
+ const run = (data) => impl(data);
314
+ this.validate = preprocess
315
+ ? (data) => { preprocess(data); return run(data); }
316
+ : run;
317
+ // What validate() returns for a document already known to fail, without
318
+ // deciding again: the lazy layer in lib/validator-core.js has its verdict
319
+ // and used to call validate() to get the errors, which ran the verdict a
320
+ // second time and built a second rejection around it.
321
+ if (!preprocess) ctx.rejectBase = onReject;
322
+ // The rich combined function, for a validator whose errors are being read
323
+ // with richErrors on. See _ensureCompiled.
324
+ if (!preprocess) {
325
+ ctx.richBuilder = richBuilder;
326
+ // The plain combined function stands in where no rich one is built
327
+ // (richErrors off, or a schema source to frame errors from): its errors
328
+ // are presented, and enriched when asked, the usual way, without the
329
+ // rejection layers between.
330
+ // Where the plain combined function declines (oneOf, anyOf, an
331
+ // additionalProperties schema), the error function stands in, called
332
+ // directly rather than through the rejection layers. Its errors can be
333
+ // collapsed branches that still need presenting; only the combined
334
+ // function's are already in their final plain shape (isCleanShape).
335
+ ctx.oneShotRich = ctx.buildRich ? richIfSafe : () => combinedIfSafe() || errFnIfSafe();
336
+ ctx.oneShotPlain = !ctx.buildRich;
337
+ ctx.isCleanShape = (fn) => fn !== null && fn === _safeCombined;
338
+ ctx.isCombined = (fn) => fn !== null && fn === _safeCombined;
339
+ // validate() as the combined function in its result shape: built and
340
+ // probed here, null when it declines or throws on the probe.
341
+ ctx.onePassOf = (empty, fallback) => {
342
+ // A collapsed branch error is built in its final plain shape here
343
+ // (no ordering key in the runtime shape); its branch errors are put
344
+ // in order with the rest.
345
+ const sort = (errs) => (_needsOrdering(errs) ? _sortErrors(schemaObj, errs) : errs);
346
+ try {
347
+ 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 } });
348
+ if (!fn) return null;
349
+ fn({}); fn(null); fn(0);
350
+ return fn;
351
+ } catch { return null; }
352
+ };
353
+ }
354
+ } else {
355
+ // No hybrid factory, so the assembly needs the function itself rather
356
+ // than a reference it can call later: build it now.
357
+ const safeCombinedFn = combinedIfSafe();
358
+ if (safeCombinedFn) {
359
+ this.validate = preprocess
360
+ ? (data) => { preprocess(data); return safeCombinedFn(data); }
361
+ : safeCombinedFn;
362
+ if (!preprocess) ctx.rejectBase = (data) => _mustReject(safeCombinedFn(data));
363
+ } else {
364
+ this.validate = preprocess
365
+ ? (data) => {
366
+ preprocess(data);
367
+ return jsFn(data) ? VALID_RESULT : errOnly(data);
368
+ }
369
+ : (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
370
+ if (!preprocess) ctx.rejectBase = errOnly;
371
+ }
372
+ }
373
+ // Verbose mode: populate parentSchema, schema and data on each error, the
374
+ // three fields the default error shape carries under the same option.
375
+ // `data` is the value the error points at; without it a caller has to
376
+ // walk the document by the instance path itself, which is what one
377
+ // migration ended up writing by hand. Errors may be frozen, so clone
378
+ // them with the extra fields.
379
+ if (this._verbose) {
380
+ // The verbose fields are added here, so a path around this layer would
381
+ // miss them.
382
+ ctx.rejectBase = null;
383
+ ctx.oneShotRich = null;
384
+ this.validate = _verboseWrap(this.validate, this._schemaObj);
385
+ }
386
+ // The verdict methods answer validate()'s question without building the
387
+ // error list, so they run the same preprocess pass. Skipping it made the
388
+ // two disagree on input that coercion or a default would have fixed.
389
+ _bindVerdict(this, fusedRemove
390
+ ? (data) => fusedRemove(data) || (preprocess(data), jsFn(data))
391
+ : preprocess
392
+ ? (data) => { preprocess(data); return jsFn(data) }
393
+ : jsFn);
394
+ // Same preference as the object path: the combined function first, since
395
+ // it validates and collects in one pass, and the error generator behind
396
+ // it. `errPreferCombined` is that order, resolved on the first rejection
397
+ // instead of at compile time.
398
+ // Tiered the same way as validate() above.
399
+ let jsonHybrid = null;
400
+ let jsonWarm = 0;
401
+ const jsonValidateTiered = (obj) => {
402
+ if (jsonHybrid !== null) return jsonHybrid(obj);
403
+ if (++jsonWarm >= HYBRID_TIER_CALLS) {
404
+ jsonHybrid = (jsFn._hybridFactory && jsFn._hybridFactory(VALID_RESULT, errPreferCombined))
405
+ || ((o) => (jsFn(o) ? VALID_RESULT : errPreferCombined(o)));
406
+ return jsonHybrid(obj);
407
+ }
408
+ return jsFn(obj) ? VALID_RESULT : errPreferCombined(obj);
409
+ };
410
+ // Parsed text takes the same preprocess pass as a parsed object, so
411
+ // validate(obj) and validateJSON(text) answer the same for the same
412
+ // document. Without it, coercion, removal and defaults applied on one
413
+ // path and not the other.
414
+ // abortEarly holds for the text path too: the stub, not errors, as the
415
+ // scanner below and the interpreted engine already answered.
416
+ const jsonValidateInner = options.abortEarly
417
+ ? (obj) => (jsFn(obj) ? VALID_RESULT : ABORT_EARLY_RESULT)
418
+ : jsonValidateTiered;
419
+ const jsonValidateFn = preprocess
420
+ ? (obj) => { preprocess(obj); return jsonValidateInner(obj) }
421
+ : jsonValidateInner;
422
+ // A document at or above the simdjson threshold is answered by the native
423
+ // walker without being parsed, except for the shapes lib/buffer-gate.js
424
+ // lists, where the walker disagrees with validate(); those schemas never
425
+ // ask it. A rejection's errors come from the same path as a small
426
+ // document's, on first read. The addon's own validateJSON used to supply
427
+ // them and accepted documents validate() rejects, 231 of the official
428
+ // suite's cases once padded past the threshold.
429
+ let nativeText; // undefined until the first large document
430
+ const nativeVerdict = (jsonStr) => {
431
+ if (nativeText === undefined) {
432
+ nativeText = !!getNative() && !require('./buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords);
433
+ if (nativeText) {
434
+ this._ensureNative();
435
+ if (!(this._fastSlot >= 0)) nativeText = false;
436
+ }
437
+ }
438
+ return nativeText ? getNative().rawFastValidate(this._fastSlot, Buffer.from(jsonStr)) : undefined;
439
+ };
440
+ // The parsed document is left on `_lastParsed` for the rich layer around
441
+ // validateJSON, which hands it to the rejection so reading the errors does
442
+ // not parse the text again (see LazyJsonRejection).
443
+ const validateText = (jsonStr) => {
444
+ let obj;
445
+ try {
446
+ obj = JSON.parse(jsonStr);
447
+ } catch (e) {
448
+ if (!(e instanceof SyntaxError)) throw e;
449
+ return _jsonSyntaxRejection(e);
450
+ }
451
+ this._lastParsed = obj;
452
+ return jsonValidateFn(obj);
453
+ };
454
+ this.validateJSON = useSimdjsonForLarge && !preprocess
455
+ ? (jsonStr) => {
456
+ // `_skipNativeFast` is set by the scanner short-circuit below when it
457
+ // has already decided the document is invalid. The encode and the
458
+ // native call would run only to return false.
459
+ if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
460
+ const ok = nativeVerdict(jsonStr);
461
+ if (ok === true) return VALID_RESULT;
462
+ if (ok === false) return options.abortEarly ? ABORT_EARLY_RESULT : new TextRejection(jsonStr, validateText);
463
+ }
464
+ return validateText(jsonStr);
465
+ }
466
+ : validateText;
467
+ // The addon validates the bytes as they are, which is the wrong answer
468
+ // when the schema asks for coercion, removal or defaults: those change
469
+ // what counts as valid. With a preprocess pass configured the text is
470
+ // parsed and run through the same path validate() takes.
471
+ const verdictFromText = (jsonStr) => {
472
+ let parsed;
473
+ try {
474
+ parsed = JSON.parse(jsonStr);
475
+ } catch (e) {
476
+ if (!(e instanceof SyntaxError)) throw e;
477
+ return false;
478
+ }
479
+ if (preprocess) preprocess(parsed);
480
+ return jsFn(parsed);
481
+ };
482
+ this.isValidJSON = useSimdjsonForLarge && !preprocess
483
+ ? (jsonStr) => {
484
+ if (jsonStr.length >= SIMDJSON_THRESHOLD) {
485
+ const ok = nativeVerdict(jsonStr);
486
+ if (ok !== undefined) return ok;
487
+ }
488
+ return verdictFromText(jsonStr);
489
+ }
490
+ : verdictFromText;
491
+
492
+ // A schema-directed scanner answers the verdict from the JSON text
493
+ // without building the document. Parsing is around three quarters of the
494
+ // cost of a real request, and a caller that only wants yes or no should
495
+ // not pay it; a rejection can also stop at the byte that caused it
496
+ // instead of parsing the rest of a document that is already refused.
497
+ //
498
+ // It is wired only where the verdict IS the answer. On a path that has
499
+ // to produce errors, scanning an invalid document is work thrown away,
500
+ // so those keep parsing. `abortEarly` has no errors to produce, so it
501
+ // counts as a verdict path.
502
+ //
503
+ // Not wired when a preprocess pass is configured: coercion, removal and
504
+ // defaults rewrite the document before it is judged, and the scanner
505
+ // reads what arrived. The compiler declines any schema it cannot answer
506
+ // and a compiled scanner returns BAIL for a document shape it cannot
507
+ // answer, and then the parse path below takes over unchanged.
508
+ };
509
+
510
+ // The verdict function alone, for isValidObject() before a full compile: the
511
+ // error and combined generators are left for the first rejection.
512
+ installCodegenPaths.compileVerdict = function compileVerdict () {
513
+ const { compileCacheKey, _compileCache, _compileCacheSet, _bindVerdict, _rememberInstance } = core._internals;
514
+ if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
515
+ const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
516
+ const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
517
+ // Custom formats are JS functions: skip the shared cache so different
518
+ // validators with the same schema string but different formats don't collide.
519
+ const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
520
+ if (cached && cached.jsFn) {
521
+ this._jsFn = cached.jsFn;
522
+ _bindVerdict(this, cached.jsFn);
523
+ _rememberInstance(this);
524
+ return;
525
+ }
526
+ const uf = this._userFormats;
527
+ // Custom keywords: see the note on the same call in lib/validator-core.js.
528
+ const _cg = compileToJSCodegen(this._schemaObj, sm, uf, this._keywords ? { keywords: this._keywords } : undefined);
529
+ const jsFn = _cg || (this._usesKeywords ? null : compileToJS(this._schemaObj, null, sm));
530
+ this._jsFn = jsFn;
531
+ if (jsFn) {
532
+ _bindVerdict(this, jsFn);
533
+ _rememberInstance(this);
534
+ // A partial entry: the verdict function is real, the other two are not
535
+ // built yet rather than declined. `undefined` is the not-built marker
536
+ // the full compile's _buildErr/_buildCombined look for; `null` would read as
537
+ // "the compiler declined" and cost the schema its error function, which
538
+ // is the bug this cache had once already. `isCodegen` rides along so a
539
+ // validator that later reuses this entry reports the same engine it
540
+ // would have compiled to.
541
+ if (!uf && !this._keywords) {
542
+ if (!cached) _compileCacheSet(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
543
+ else cached.jsFn = jsFn;
544
+ }
545
+ }
546
+ };
547
+
548
+ // validateJSON and isValidJSON over a schema-directed scanner, which answers
549
+ // from the JSON text without building the document.
550
+ installCodegenPaths.installScanner = function installScanner (schemaObj, options) {
551
+ const { ABORT_EARLY_RESULT, VALID_RESULT, _bindEntry, _jsonSyntaxRejection } = core._internals;
552
+ let textRejects = 0;
553
+ const self = this;
554
+ // Generating a scanner costs about 20 microseconds, measured, and it
555
+ // saves from around 85 nanoseconds on a small accepted document to
556
+ // several microseconds on a rejected one. Building it on the first
557
+ // call would therefore be a straight loss for a caller that checks one
558
+ // document and exits, so it is built once a caller has asked often
559
+ // enough that it is plainly doing this in a loop. A server passes the
560
+ // line during warm-up and never sees it.
561
+ const SCAN_AFTER = 64;
562
+ let calls = 0;
563
+ // undefined: not built. null: this schema has no scanner. Passing true
564
+ // builds it now, which is how the differential test reaches it.
565
+ this._ensureScanner = (now) => {
566
+ if (self._scanner === undefined) {
567
+ // The scanner reads the schema's own keywords from the text and knows
568
+ // nothing of custom ones, so it would accept what they reject.
569
+ if (self._usesKeywords) { self._scanner = null; return null; }
570
+ if (!now && ++calls < SCAN_AFTER) return undefined;
571
+ const built = require('./scan-compiler').compileScanner(schemaObj, { userFormats: self._userFormats });
572
+ self._scanner = built ? built.scan : null;
573
+ }
574
+ return self._scanner;
575
+ };
576
+ const byParsing = this.isValidJSON;
577
+ // The verdict is a pure function of the text, and the caller a
578
+ // gateway or a drift monitor keeps asking about is usually the same
579
+ // text: a config file re-read on a timer, a heartbeat body. One
580
+ // remembered (text, verdict) pair answers that case with a native
581
+ // string compare, which is a memcmp, instead of a scan. Withheld when
582
+ // user formats or custom keywords are present, since those are user
583
+ // functions and nothing guarantees they are pure.
584
+ const memoizable = !self._userFormats && !self._usesKeywords;
585
+ let _memoText = null;
586
+ let _memoVerdict = false;
587
+ this.isValidJSON = (jsonStr) => {
588
+ const scan = self._ensureScanner();
589
+ if (scan === undefined) return byParsing(jsonStr);
590
+ if (scan === null) { _bindEntry(self, 'isValidJSON', byParsing); return byParsing(jsonStr); }
591
+ _bindEntry(self, 'isValidJSON', memoizable
592
+ ? (text) => {
593
+ if (typeof text !== 'string') return byParsing(text);
594
+ if (text === _memoText) return _memoVerdict;
595
+ const r = scan(text);
596
+ const verdict = r === -1 ? byParsing(text) : r === 1;
597
+ _memoText = text;
598
+ _memoVerdict = verdict;
599
+ return verdict;
600
+ }
601
+ : (text) => {
602
+ if (typeof text !== 'string') return byParsing(text);
603
+ const r = scan(text);
604
+ if (r === -1) return byParsing(text);
605
+ return r === 1;
606
+ });
607
+ return self.isValidJSON(jsonStr);
608
+ };
609
+ // validateJSON gets the same short-circuit isValidJSON has. The verdict is
610
+ // a property of the text, and the scanner reads the text once and
611
+ // allocates nothing; above the simdjson threshold the path underneath
612
+ // encoded the whole document to a Buffer and called the native validator
613
+ // instead, which measured 340 microseconds against the scanner's 191 on a
614
+ // 149 KB config, the same against the published addon as against a local
615
+ // build.
616
+ //
617
+ // An accepted document stops at the scanner. A rejected one still has to
618
+ // produce errors, so it goes on to the path below, which is the
619
+ // rich-errors wrapper and everything under it; the flag only tells that
620
+ // path to skip an encode and a native call that would return false. Doing
621
+ // it the other way, returning errors from the inner function directly,
622
+ // would hand back errors that never passed through enrichment.
623
+ {
624
+ const validateByParsing = this.validateJSON;
625
+ const abortEarly = !!options.abortEarly;
626
+ // A validator whose errors are being read answers validate() with
627
+ // the one-pass function (the switch in lib/validator-core.js), and
628
+ // without richErrors nothing it hands out depends on the text. A
629
+ // rejected text is then parsed and given to that function directly,
630
+ // instead of to the layers below, which parsed it, decided again and
631
+ // wrapped a rejection whose errors are built on first read: a 1.7 KB
632
+ // rejected body took 9.2 microseconds through those layers.
633
+ // The scanner itself is the other half of that cost on a rejected
634
+ // text (2.7 of the 9.2 on that body) and all the saving on an accepted
635
+ // one, which it answers without parsing. So the scan runs while the
636
+ // texts a validator sees are mostly accepted, and is skipped while
637
+ // they are mostly rejected: `bias` counts rejections against
638
+ // acceptances, from either path, and the mode follows its sign with
639
+ // some room, so a mixed stream does not flip on every document.
640
+ const onePassText = (text) => {
641
+ let op = self._onePass;
642
+ // A validator rejecting text only never reads through the object
643
+ // path's switch; the second rejected text makes it.
644
+ if (op === undefined && self._tierOnePass !== undefined && ++textRejects >= 2) { op = self._tierOnePass() || undefined; if (op === undefined) self._tierOnePass = undefined; }
645
+ if (op === undefined) return undefined;
646
+ let obj;
647
+ try { obj = JSON.parse(text); } catch (e) { if (!(e instanceof SyntaxError)) throw e; return _jsonSyntaxRejection(e); }
648
+ return op(obj);
649
+ };
650
+ this.validateJSON = (jsonStr) => {
651
+ const scan = self._ensureScanner();
652
+ if (scan === undefined) return validateByParsing(jsonStr);
653
+ if (scan === null) {
654
+ // No scanner for this schema: the one-pass function still answers a
655
+ // lean validator's texts directly once it is installed.
656
+ _bindEntry(self, 'validateJSON', self._richErrors ? validateByParsing : (text) => {
657
+ if (typeof text === 'string') { const o = onePassText(text); if (o !== undefined) return o; }
658
+ return validateByParsing(text);
659
+ });
660
+ return self.validateJSON(jsonStr);
661
+ }
662
+ let bias = 0;
663
+ _bindEntry(self, 'validateJSON', (text) => {
664
+ if (typeof text === 'string') {
665
+ if (bias > 16 && !self._richErrors && self._onePass !== undefined) {
666
+ const r = onePassText(text);
667
+ if (r !== undefined) { if (r.valid) { if (--bias < -32) bias = -32; } else if (bias < 64) bias++; return r; }
668
+ }
669
+ const r = scan(text);
670
+ if (r === 1) { if (--bias < -32) bias = -32; return VALID_RESULT; }
671
+ if (r === 0) {
672
+ if (bias < 64) bias++;
673
+ if (abortEarly) return ABORT_EARLY_RESULT;
674
+ if (!self._richErrors) {
675
+ const o = onePassText(text);
676
+ if (o !== undefined) return o;
677
+ }
678
+ self._skipNativeFast = true;
679
+ try {
680
+ return validateByParsing(text);
681
+ } finally {
682
+ self._skipNativeFast = false;
683
+ }
684
+ }
685
+ }
686
+ // The scanner could not decide (a shape it does not read): the
687
+ // one-pass function answers a lean validator here too.
688
+ if (!self._richErrors && typeof text === 'string') { const o = onePassText(text); if (o !== undefined) return o; }
689
+ return validateByParsing(text);
690
+ });
691
+ return self.validateJSON(jsonStr);
692
+ };
693
+ }
694
+ };
695
+
696
+ // The defaults, coercion and removal pass as generated source, which runs
697
+ // about twelve times faster than the closure mutators in validator-core.js.
698
+ // ata-validator/lite takes the closures.
699
+ // Emit the in-place strip for one schema node and everything under its
700
+ // `properties`. Scope matches collectRemovals(): object properties only, so
701
+ // the two paths keep the same answer.
702
+ // Each level's code, its own removal loop and its children's, sits inside the
703
+ // guard for that level. A child used to be guarded only on itself, so its
704
+ // access expression read through a parent that could be absent or null:
705
+ // `{ c: { properties: { d: { additionalProperties: false } } } }` threw a
706
+ // TypeError from validate({}) whenever the optional `c` was missing.
707
+ function emitRemovals(node, access, lines, depth, seen) {
708
+ if (!node || typeof node !== 'object' || !node.properties) return;
709
+ if (seen.has(node)) return;
710
+ seen.add(node);
711
+ const keys = Object.keys(node.properties);
712
+ const body = [];
713
+ // With no declared property every key is additional, as the interpreted
714
+ // engine's remover has it; this pass used to skip such a node entirely.
715
+ if (node.additionalProperties === false) {
716
+ const kv = '_k' + depth;
717
+ const checks = keys.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&');
718
+ body.push(keys.length > 0
719
+ ? `for(var ${kv} in ${access})if(${checks})delete ${access}[${kv}]`
720
+ : `for(var ${kv} in ${access})delete ${access}[${kv}]`);
721
+ }
722
+ for (const key of keys) {
723
+ const prop = node.properties[key];
724
+ if (prop && typeof prop === 'object' && prop.properties) {
725
+ emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, body, depth + 1, seen);
726
+ }
727
+ }
728
+ seen.delete(node);
729
+ if (body.length === 0) return;
730
+ if (depth === 0) lines.push(...body);
731
+ else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
732
+ }
733
+
734
+ // The coercions the closure pass applies, emitted. Values: `number` and
735
+ // `integer` from numeric strings and booleans, `string` from numbers and
736
+ // booleans, `boolean` from "true"/"1" and "false"/"0".
737
+ const COERCIBLE_TYPES = new Set(['number', 'integer', 'string', 'boolean']);
738
+ function scalarCoercion(a, t) {
739
+ if (t === 'integer') return [`if(typeof ${a}==='string'){var _n=Number(${a});if(${a}!==''&&Number.isInteger(_n))${a}=_n}`, `if(typeof ${a}==='boolean')${a}=${a}?1:0`];
740
+ if (t === 'number') return [`if(typeof ${a}==='string'){var _n=Number(${a});if(${a}!==''&&!isNaN(_n))${a}=_n}`, `if(typeof ${a}==='boolean')${a}=${a}?1:0`];
741
+ if (t === 'string') return [`if(typeof ${a}==='number'||typeof ${a}==='boolean')${a}=String(${a})`];
742
+ return [`if(${a}==='true'||${a}==='1')${a}=true`, `if(${a}==='false'||${a}==='0')${a}=false`];
743
+ }
744
+ function isScalarCoercible(node) {
745
+ return !!(node && typeof node === 'object' && typeof node.type === 'string' && COERCIBLE_TYPES.has(node.type));
746
+ }
747
+ // Whether anything below `node` is coerced, as buildNodeCoercer decides it.
748
+ // Remembered per node for the build, since every enclosing node asks again.
749
+ function coercesInside(node, seen, memo) {
750
+ if (!node || typeof node !== 'object' || seen.has(node)) return false;
751
+ if (memo && memo.has(node)) return memo.get(node);
752
+ seen.add(node);
753
+ let found = false;
754
+ if (node.properties) {
755
+ for (const [key, prop] of Object.entries(node.properties)) {
756
+ if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
757
+ if (isScalarCoercible(prop) || coercesInside(prop, seen, memo)) { found = true; break; }
758
+ }
759
+ }
760
+ if (!found && node.items && typeof node.items === 'object' && !Array.isArray(node.items)) {
761
+ found = isScalarCoercible(node.items) || coercesInside(node.items, seen, memo);
762
+ }
763
+ seen.delete(node);
764
+ if (memo) memo.set(node, found);
765
+ return found;
766
+ }
767
+ function emitCoercions(node, ov, lines, st, depth) {
768
+ if (st.seen.has(node)) { st.cycle = true; return; }
769
+ st.seen.add(node);
770
+ if (node.properties) {
771
+ for (const [key, prop] of Object.entries(node.properties)) {
772
+ // Coercion writes with plain assignment, which for a key named
773
+ // __proto__ rewrites the prototype instead. The raw value still goes
774
+ // through validation, so skipping is a refusal to coerce, not a hole.
775
+ if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
776
+ const k = JSON.stringify(key);
777
+ const a = `${ov}[${k}]`;
778
+ if (isScalarCoercible(prop)) lines.push(...scalarCoercion(a, prop.type));
779
+ // Wrapping a lone value in an array is a top-level rule only, as it was.
780
+ else if (depth === 0 && prop.type === 'array' && st.arrayMode) lines.push(`if(${k} in ${ov}&&${a}!==undefined&&!Array.isArray(${a}))${a}=[${a}]`);
781
+ // A leaf, the common case, has nothing below it to coerce.
782
+ if ((prop.properties || prop.items) && coercesInside(prop, new Set(), st.memo)) {
783
+ const n = '_c' + st.n++;
784
+ lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null){`);
785
+ emitCoercions(prop, n, lines, st, depth + 1);
786
+ lines.push('}}');
787
+ }
788
+ }
789
+ }
790
+ const it = node.items;
791
+ if (it && typeof it === 'object' && !Array.isArray(it) && (isScalarCoercible(it) || coercesInside(it, new Set(), st.memo))) {
792
+ const i = '_i' + st.n++;
793
+ lines.push(`if(Array.isArray(${ov}))for(let ${i}=0;${i}<${ov}.length;${i}++){`);
794
+ if (isScalarCoercible(it)) lines.push(...scalarCoercion(`${ov}[${i}]`, it.type));
795
+ if (coercesInside(it, new Set(), st.memo)) {
796
+ const n = '_c' + st.n++;
797
+ lines.push(`{const ${n}=${ov}[${i}];if(typeof ${n}==='object'&&${n}!==null){`);
798
+ emitCoercions(it, n, lines, st, depth + 1);
799
+ lines.push('}}');
800
+ }
801
+ lines.push('}');
802
+ }
803
+ st.seen.delete(node);
804
+ }
805
+ function hasDefaultsInside(node, seen) {
806
+ if (!node || typeof node !== 'object' || !node.properties || seen.has(node)) return false;
807
+ seen.add(node);
808
+ let found = false;
809
+ for (const prop of Object.values(node.properties)) {
810
+ if (prop && typeof prop === 'object' && (prop.default !== undefined || hasDefaultsInside(prop, seen))) { found = true; break; }
811
+ }
812
+ seen.delete(node);
813
+ return found;
814
+ }
815
+ function emitDefaults(node, ov, lines, st) {
816
+ if (st.seen.has(node)) { st.cycle = true; return; }
817
+ st.seen.add(node);
818
+ // A default is written only where the key is not an own property, and the
819
+ // own-property test used to be Object.hasOwn for every default on every
820
+ // call: on a configuration schema of 82 defaults that was 820 ns a call
821
+ // for a document that already carried every key, against 12 ns with the
822
+ // pass off. For a plain object (prototype Object.prototype) a key the
823
+ // prototype does not have is own exactly when reading it gives a value, so
824
+ // the read, an inline-cached load, settles the common case and hasOwn is
825
+ // consulted only when the read gives undefined. Keys Object.prototype does
826
+ // have (constructor, toString, __proto__) keep the hasOwn test, since the
827
+ // read would find the inherited one; so does any object with another
828
+ // prototype, where a read may find an inherited value of the same name.
829
+ const pk = '_pk' + st.n++;
830
+ lines.push(`const ${pk}=Object.getPrototypeOf(${ov})===Object.prototype`);
831
+ for (const [key, prop] of Object.entries(node.properties || {})) {
832
+ if (!prop || typeof prop !== 'object') continue;
833
+ const k = JSON.stringify(key);
834
+ const inherited = key in Object.prototype;
835
+ const absent = inherited ? `!Object.hasOwn(${ov},${k})` : `(${ov}[${k}]===undefined||!${pk})&&!Object.hasOwn(${ov},${k})`;
836
+ if (prop.default !== undefined) {
837
+ const def = JSON.stringify(prop.default);
838
+ // Assignment to a key named __proto__ hits the prototype setter
839
+ // instead of creating a property; defineProperty writes an own key.
840
+ lines.push(key === '__proto__'
841
+ ? `if(${absent})Object.defineProperty(${ov},${k},{value:${def},writable:true,enumerable:true,configurable:true})`
842
+ : `if(${absent})${ov}[${k}]=${def}`);
843
+ }
844
+ // Into an own property that holds an object, arrays included, as the
845
+ // closure pass walks it. Object.prototype's own properties are functions
846
+ // apart from the __proto__ accessor, so on a plain object a read that
847
+ // gives an object is an own property for every other key.
848
+ if (prop.properties && hasDefaultsInside(prop, new Set())) {
849
+ const n = '_d' + st.n++;
850
+ const own = inherited ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
851
+ lines.push(`{const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
852
+ emitDefaults(prop, n, lines, st);
853
+ lines.push('}}');
854
+ }
855
+ }
856
+ st.seen.delete(node);
857
+ }
858
+
859
+ // Generate a fast preprocess function via codegen instead of closure arrays
860
+ function buildPreprocessCodegen(schema, options) {
861
+ if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
862
+ const lines = [];
863
+
864
+ // removeAdditional: strip unknown keys at every level the schema describes,
865
+ // not just the top one. The closure path below (collectRemovals) always
866
+ // recursed; this one did not, so the same schema and the same document got
867
+ // opposite verdicts depending on whether the runtime allowed code
868
+ // generation, and the codegen answer was the one that disagreed with both
869
+ // the interpreter and the default validator this aims to match.
870
+ if (options.removeAdditional) {
871
+ emitRemovals(schema, 'd', lines, 0, new Set());
872
+ }
873
+
874
+ // Coercion and defaults reach every depth the closure passes in
875
+ // validator-core.js reach, with the same rules in the same order, so the
876
+ // interpreted engine, which uses those passes, gives the same answer. Both
877
+ // used to stop at the top-level properties. A schema object that contains
878
+ // itself declines here, and the closure passes, which carry a guard for it,
879
+ // take the schema.
880
+ const st = { n: 0, seen: new Set(), cycle: false, arrayMode: options.coerceTypes === 'array', memo: new Map() };
881
+ if (options.coerceTypes) emitCoercions(schema, 'd', lines, st, 0);
882
+ // hasDefaultsInside answers without emitting; most schemas have no default,
883
+ // and the emitting walk allocates for every property it visits.
884
+ if (options.useDefaults !== false && hasDefaultsInside(schema, new Set())) emitDefaults(schema, 'd', lines, st);
885
+ if (st.cycle) return null;
886
+
887
+ if (lines.length === 0) return null;
888
+ // Data may legitimately be null or a non-object (e.g. a `['object','null']`
889
+ // schema), so the per-property mutations must not run on it.
890
+ lines.unshift(`if(d===null||typeof d!=='object')return`);
891
+ // The pass depends on property names, types, defaults and which objects
892
+ // are closed, not on the constraints the verdict checks, so routes that
893
+ // take the same shape with different limits (a page/limit querystring, an
894
+ // id param) emit the same source. Compiling it is most of what building the
895
+ // pass costs, so the function is kept by its source and compiled once. It
896
+ // holds no state: it rewrites the object it is given and nothing else.
897
+ const src = lines.join('\n');
898
+ let fn = _preprocessBySource.get(src);
899
+ if (fn === undefined) {
900
+ try {
901
+ fn = compileFunction('d', src);
902
+ } catch {
903
+ fn = null;
904
+ }
905
+ if (_preprocessBySource.size >= PREPROCESS_SOURCE_LIMIT) _preprocessBySource.clear();
906
+ _preprocessBySource.set(src, fn);
907
+ }
908
+ return fn;
909
+ }
910
+ const _preprocessBySource = new Map();
911
+ const PREPROCESS_SOURCE_LIMIT = 4096;
912
+
913
+
914
+ // parse() for the full package: a generated function that copies the keys the
915
+ // schema declares, behind the generated verdict. The refusals every build
916
+ // shares stay in validator-core.js; this is the part that needs the code
917
+ // generator.
918
+ function buildParse (self, decline, extended) {
919
+ const { cloneExprFor } = require('./clone-emit');
920
+ const expr = cloneExprFor(self._schemaObj);
921
+ if (!expr) return decline('the set of keys to keep cannot be proven from the schema');
922
+ let copy;
923
+ try {
924
+ // eslint-disable-next-line no-new-func
925
+ copy = compileFunction('data', 'return ' + expr);
926
+ } catch {
927
+ return decline('code generation is not allowed here');
928
+ }
929
+ self._ensureCompiled();
930
+ const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
931
+ if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
932
+ return (data) => {
933
+ if (!verdict(data)) {
934
+ const e = new Error('validation failed');
935
+ e.name = 'AtaValidationError';
936
+ let target = data;
937
+ if (self._mutatesInput) {
938
+ try { target = structuredClone(data); } catch { target = data; }
939
+ }
940
+ // The errors are worked out when first read, as validate() works them
941
+ // out: building every error with its enrichment before throwing made a
942
+ // rejected parse() cost about 12 µs on a document with 16 violations,
943
+ // most of it for callers that only catch. Read once, the list stays.
944
+ const rejection = self.validate(target);
945
+ Object.defineProperty(e, 'errors', {
946
+ configurable: true,
947
+ enumerable: true,
948
+ get () {
949
+ const errors = rejection.errors;
950
+ Object.defineProperty(e, 'errors', { value: errors, writable: true, configurable: true, enumerable: true });
951
+ return errors;
952
+ },
953
+ set (v) { Object.defineProperty(e, 'errors', { value: v, writable: true, configurable: true, enumerable: true }); },
954
+ });
955
+ throw e;
956
+ }
957
+ return copy(data);
958
+ };
959
+ }
960
+
961
+ // Everything that turns a schema into JavaScript source, registered with the
962
+ // core: the three code generators and the paths above, the generated
963
+ // preprocess pass, the JSON-text scanner, the parse() copy and the
964
+ // ahead-of-time bundle methods. parse() and the bundle methods load on first
965
+ // use.
966
+ // Cold start. A validator's first COLD_CALLS calls are answered by an
967
+ // interpreted twin, built from the same schema and options with
968
+ // `engine: 'interpreter'`, which the test suite holds to the same answers and
969
+ // errors as the generated code. Generating code and having V8 compile it is
970
+ // the largest part of a first call on a large schema: from process start to
971
+ // one rejection with its error read, SchemaStore's SARIF schema took 87 ms
972
+ // with code generated at the first call and 31 ms with the twin answering.
973
+ // A validator still in use after that compiles as before, so a server reaches
974
+ // the generated code's speed within its first requests, and a command line
975
+ // tool or a short-lived function never pays to compile. Only where compiling
976
+ // costs enough to matter: a schema whose size with its references followed
977
+ // passes COLD_MIN characters (expandedChars in lib/js-compiler.js). A small
978
+ // schema compiles in about a millisecond and is generated at its first call
979
+ // as before, which also keeps every test that compares the generated code
980
+ // with the interpreter comparing the generated code. Not for a validator with
981
+ // an extension (_extendValidate, _extendChecks), which the twin would not
982
+ // carry. Returns the twin to answer this call, or null to compile.
983
+ const COLD_CALLS = 64;
984
+ const COLD_MIN = 16 * 1024;
985
+ // Whether the schema may expand past `limit`, answered from its text where
986
+ // that settles it. Without a `$ref` nothing expands, and expandedChars counts
987
+ // at most 8 for each character of the text, so a short schema is decided
988
+ // without walking it. The text is the one _ensureCompiled needs anyway. Walking
989
+ // every small route schema here cost a Fastify boot of ten routes 0.15 ms.
990
+ function mayExpandPast (self, limit) {
991
+ const str = self._schemaStr || (self._schemaStr = JSON.stringify(self._schemaObj));
992
+ if (str === undefined) return false;
993
+ if (str.length * 8 <= limit && !str.includes('"$ref"')) return false;
994
+ return jsCompiler.expandedChars(self._schemaObj, limit) > limit;
995
+ }
996
+
997
+ function coldTwin (self, Validator) {
998
+ const n = self._coldCalls === undefined ? 0 : self._coldCalls;
999
+ if (n >= COLD_CALLS || self._validateTail !== null || self._verdictTail !== null) return null;
1000
+ let t = self._twin;
1001
+ if (t === undefined) {
1002
+ t = null;
1003
+ if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
1004
+ try { t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' })); } catch { t = null; }
1005
+ }
1006
+ self._twin = t;
1007
+ }
1008
+ if (t === null) return null;
1009
+ self._coldCalls = n + 1;
1010
+ return t;
1011
+ }
1012
+
1013
+ module.exports = {
1014
+ jsCompiler,
1015
+ coldTwin,
1016
+ // The native locator for error frames, loaded with the first frame.
1017
+ nativePositions: () => require('./native-positions').nativeTargeted,
1018
+ installPaths: installCodegenPaths,
1019
+ compileVerdict: installCodegenPaths.compileVerdict,
1020
+ installScanner: installCodegenPaths.installScanner,
1021
+ buildPreprocess: buildPreprocessCodegen,
1022
+ buildParse,
1023
+ aot: () => require('./aot.js'),
1024
+ };