ata-validator 1.48.0 → 1.50.0

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