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/README.md +45 -7
- package/aot.d.ts +40 -0
- package/aot.mjs +1 -1
- package/bin/ata.js +14 -1
- package/build.d.ts +1 -1
- package/error-runtime.d.ts +24 -0
- package/error-runtime.js +40 -0
- package/error-runtime.mjs +3 -0
- package/index.browser.mjs +14 -5
- package/index.edge.js +14 -0
- package/index.edge.mjs +13 -0
- package/index.js +8 -996
- package/lib/aot-build.js +211 -68
- package/lib/aot-impl.js +216 -8
- package/lib/codegen-engine.js +1024 -0
- package/lib/draft7.js +106 -8
- package/lib/enrich-error.js +42 -11
- package/lib/enrich-site.js +1 -1
- package/lib/error-code-of.js +22 -0
- package/lib/error-runtime-entry.js +85 -0
- package/lib/error-runtime.js +152 -0
- package/lib/error-sort-rank.js +61 -0
- package/lib/interpreter-core.js +1356 -0
- package/lib/interpreter.js +8 -1260
- package/lib/js-compiler-errors.js +6 -4
- package/lib/js-compiler.js +50 -2
- package/lib/metaschemas.js +21 -2
- package/lib/public-api.js +80 -0
- package/lib/regex-linear.js +194 -37
- package/lib/safe-regex.js +23 -9
- package/lib/schema-runtime-entry.js +21 -0
- package/lib/schema-strip.js +115 -0
- package/lib/tools.mjs +21 -0
- package/lib/validator-core.js +35 -20
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +12 -0
- package/package.json +29 -9
|
@@ -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
|
+
};
|