ata-validator 1.34.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -15
- package/compiled.d.ts +17 -0
- package/compiled.js +7 -0
- package/compiled.mjs +3 -0
- package/index.browser.mjs +3 -2
- package/index.js +606 -2991
- package/index.mjs +1 -1
- package/lib/aot-build.js +40 -0
- package/lib/buffer-gate.js +0 -1
- package/lib/compiled.js +84 -0
- package/lib/enrich-error.js +7 -0
- package/lib/formats-source.js +323 -0
- package/lib/formats.js +1 -317
- package/lib/interpreter.js +4 -4
- package/lib/js-compiler.js +5 -5
- package/lib/rejections.js +309 -0
- package/lib/validator-core.js +2052 -0
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +1 -2
- package/package.json +17 -9
package/index.js
CHANGED
|
@@ -1,246 +1,538 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The full package: the validator core with the code generator registered,
|
|
4
|
+
// plus the tools no Validator calls (TypeScript generation, renderers, output
|
|
5
|
+
// formats). The core lives in lib/validator-core.js so that
|
|
6
|
+
// ata-validator/lite can load it without either.
|
|
7
|
+
const core = require('./lib/validator-core.js');
|
|
8
|
+
|
|
9
|
+
const jsCompiler = require('./lib/js-compiler.js');
|
|
10
|
+
const { compileToJS, compileToJSCodegen } = jsCompiler;
|
|
11
|
+
|
|
12
|
+
// The compiled paths a Validator installs when the code generator produced a
|
|
13
|
+
// verdict function: the hybrid and combined forms of validate(), the lazy
|
|
14
|
+
// error resolvers, validateJSON() over simdjson and the scanner, abortEarly.
|
|
15
|
+
// They live here rather than in Validator#_ensureCompiled so that
|
|
16
|
+
// ata-validator/lite, which never has a verdict function, does not carry
|
|
17
|
+
// them. Called with the validator as `this` and the locals of
|
|
18
|
+
// _ensureCompiled in `ctx`; the error and combined functions are built later
|
|
19
|
+
// by the builders, so they are read through ctx.err() and ctx.combined().
|
|
20
|
+
|
|
21
|
+
// A rejection the native walker decided from the text alone. The errors come
|
|
22
|
+
// from parsing and validating in JS, on first read, so a caller that reads
|
|
23
|
+
// only `.valid` never parses the document.
|
|
24
|
+
class TextRejection {
|
|
25
|
+
constructor (text, validateText) {
|
|
26
|
+
this.valid = false;
|
|
27
|
+
this._text = text;
|
|
28
|
+
this._validateText = validateText;
|
|
29
|
+
this._errors = null;
|
|
30
|
+
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Object.prototype itself. defineProperty has no such special case.
|
|
37
|
-
function setOwn(obj, key, val) {
|
|
38
|
-
if (key === "__proto__") {
|
|
39
|
-
Object.defineProperty(obj, key, {
|
|
40
|
-
value: val,
|
|
41
|
-
writable: true,
|
|
42
|
-
enumerable: true,
|
|
43
|
-
configurable: true,
|
|
44
|
-
});
|
|
45
|
-
} else {
|
|
46
|
-
obj[key] = val;
|
|
32
|
+
get errors () {
|
|
33
|
+
if (this._errors === null) this._errors = core._internals._mustReject(this._validateText(this._text)).errors;
|
|
34
|
+
return this._errors;
|
|
47
35
|
}
|
|
48
36
|
}
|
|
49
37
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
actions.push((data) => {
|
|
70
|
-
let target = data;
|
|
71
|
-
for (let j = 0; j < parentPath.length; j++) {
|
|
72
|
-
if (typeof target !== "object" || target === null) return;
|
|
73
|
-
// Own keys only. `target[key]` for an inherited name walks the
|
|
74
|
-
// prototype chain: a parent named `__proto__` that the instance
|
|
75
|
-
// does not carry resolved to Object.prototype, and the child
|
|
76
|
-
// defaults were written onto it, for every object in the realm.
|
|
77
|
-
if (!Object.hasOwn(target, parentPath[j])) return;
|
|
78
|
-
target = target[parentPath[j]];
|
|
79
|
-
}
|
|
80
|
-
if (
|
|
81
|
-
typeof target === "object" &&
|
|
82
|
-
target !== null &&
|
|
83
|
-
!Object.hasOwn(target, key)
|
|
84
|
-
) {
|
|
85
|
-
setOwn(target,
|
|
86
|
-
key,
|
|
87
|
-
typeof defaultVal === "object" && defaultVal !== null
|
|
88
|
-
? JSON.parse(JSON.stringify(defaultVal))
|
|
89
|
-
: defaultVal);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
38
|
+
function installCodegenPaths (ctx) {
|
|
39
|
+
const { ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindVerdict, _jsonSyntaxRejection, _mustReject, getNative, isV1Dialect, resolveSchemaByPath } = core._internals;
|
|
40
|
+
const { jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr } = ctx;
|
|
41
|
+
// errFn: the generated error function when it is safe, else the
|
|
42
|
+
// interpreted engine, on every platform alike.
|
|
43
|
+
const hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
|
|
44
|
+
// The interpreted engine re-validates failing data to produce full errors. If it disagrees with the codegen verdict
|
|
45
|
+
// (it should not), a generic error keeps the result consistent.
|
|
46
|
+
let _interp = null;
|
|
47
|
+
const jsOnlyFallback = (d) => {
|
|
48
|
+
if (jsFn(d)) return { valid: true, data: d, errors: [] };
|
|
49
|
+
if (!_interp) {
|
|
50
|
+
const { createInterpreter } = require('./lib/interpreter');
|
|
51
|
+
_interp = createInterpreter(schemaObj, {
|
|
52
|
+
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
53
|
+
formats: this._userFormats,
|
|
54
|
+
v1: isV1Dialect(schemaObj),
|
|
55
|
+
keywords: this._keywords,
|
|
56
|
+
});
|
|
93
57
|
}
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
58
|
+
const r = _interp.validate(d);
|
|
59
|
+
if (!r.valid) return r;
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
errors: [{
|
|
63
|
+
keyword: 'validation',
|
|
64
|
+
instancePath: '',
|
|
65
|
+
schemaPath: '',
|
|
66
|
+
params: {},
|
|
67
|
+
message: 'schema validation failed'
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
// The error generator declines unevaluated*; the interpreted engine
|
|
72
|
+
// reports those schemas correctly, so failing data is re-validated
|
|
73
|
+
// there. This used to be a placeholder error with no keyword and no
|
|
74
|
+
// path, which hid whatever had actually failed.
|
|
75
|
+
// Resolved on the first rejection rather than at compile time, because
|
|
76
|
+
// building the generator behind it is two thirds of what a first call
|
|
77
|
+
// costs and a caller that never reads an error never needs it. The probe
|
|
78
|
+
// moves here with it: it calls the generated function, so it cannot run
|
|
79
|
+
// before the function exists.
|
|
80
|
+
let _errOnlyImpl = null;
|
|
81
|
+
const errOnly = (d) => {
|
|
82
|
+
if (_errOnlyImpl === null) {
|
|
83
|
+
_buildErr();
|
|
84
|
+
let safe = null;
|
|
85
|
+
if (ctx.err()) {
|
|
86
|
+
try {
|
|
87
|
+
ctx.err()({}, true);
|
|
88
|
+
safe = (x) => ctx.err()(x, true);
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
// Where the generator declines, the interpreted engine answers. It used
|
|
92
|
+
// to be the native addon when one was installed, whose errors carry
|
|
93
|
+
// other wording and no schemaPath, so the same validator reported
|
|
94
|
+
// differently depending on what the platform had installed.
|
|
95
|
+
_errOnlyImpl = safe || jsOnlyFallback;
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Build a function that coerces property values to match schema types in-place.
|
|
102
|
-
// Handles string→number, string→integer, string→boolean, number→string, boolean→string.
|
|
103
|
-
function buildCoercer(schema) {
|
|
104
|
-
if (typeof schema !== "object" || schema === null) return null;
|
|
105
|
-
const actions = [];
|
|
106
|
-
collectCoercions(schema, actions);
|
|
107
|
-
if (actions.length === 0) return null;
|
|
108
|
-
return (data) => {
|
|
109
|
-
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
97
|
+
return _mustReject(_errOnlyImpl(d));
|
|
110
98
|
};
|
|
111
|
-
}
|
|
112
99
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
// Best path: combined validator (single pass, validates + collects errors)
|
|
101
|
+
// Valid data: returns VALID_RESULT, no allocation
|
|
102
|
+
// Invalid data: collects errors in one pass (no double validation)
|
|
103
|
+
// Fallback: hybridFn or jsFn + errFn for schemas combined can't handle
|
|
104
|
+
// Test combined at compile time -- some schemas produce broken combined code
|
|
105
|
+
// Test combined at compile time -- some schemas (e.g. if/then/else)
|
|
106
|
+
// produce broken combined code that crashes on certain inputs.
|
|
107
|
+
// We probe with diverse data; if any throws, fall back to hybrid.
|
|
108
|
+
let _combinedProbed = false;
|
|
109
|
+
let _safeCombined = null;
|
|
110
|
+
const combinedIfSafe = () => {
|
|
111
|
+
if (_combinedProbed) return _safeCombined;
|
|
112
|
+
_combinedProbed = true;
|
|
113
|
+
_buildCombined();
|
|
114
|
+
if (ctx.combined()) {
|
|
115
|
+
try {
|
|
116
|
+
const probe = {};
|
|
117
|
+
// Populate probe with one key per known property to trigger nested paths
|
|
118
|
+
if (schemaObj && schemaObj.properties) {
|
|
119
|
+
for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
|
|
120
|
+
}
|
|
121
|
+
if (schemaObj && schemaObj.if && schemaObj.if.properties) {
|
|
122
|
+
for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
|
|
123
|
+
}
|
|
124
|
+
ctx.combined()(probe);
|
|
125
|
+
ctx.combined()({});
|
|
126
|
+
ctx.combined()(null);
|
|
127
|
+
ctx.combined()(0);
|
|
128
|
+
_safeCombined = ctx.combined();
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
return _safeCombined;
|
|
132
|
+
};
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
// What the hybrid path hands to its error slot: the combined function
|
|
135
|
+
// when it is usable, since it validates and collects in one pass, and
|
|
136
|
+
// the error generator otherwise. Same order the eager code chose, just
|
|
137
|
+
// chosen on the first rejection.
|
|
138
|
+
let _errPreferredImpl = null;
|
|
139
|
+
const errPreferCombined = (d) => {
|
|
140
|
+
if (_errPreferredImpl === null) _errPreferredImpl = combinedIfSafe() || errOnly;
|
|
141
|
+
return _mustReject(_errPreferredImpl(d));
|
|
142
|
+
};
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
// The boolean engine is the verdict authority for these paths; the
|
|
145
|
+
// final lazy wrapper uses it to skip error construction entirely.
|
|
146
|
+
if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
|
|
147
|
+
|
|
148
|
+
if (options.abortEarly && jsFn && !hasDynRef) {
|
|
149
|
+
// abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
|
|
150
|
+
// This is the perf-critical path for edge gateways. The richErrors wrap
|
|
151
|
+
// below recognises the ATA9000 stub keyword and passes the frozen result
|
|
152
|
+
// through unchanged, so a single shared object is returned per failure.
|
|
153
|
+
const _fn = jsFn;
|
|
154
|
+
this.validate = preprocess
|
|
155
|
+
? (data) => { preprocess(data); return _fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
|
|
156
|
+
: (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
|
|
157
|
+
} else if (hasDynRef && _isCodegen && jsFn) {
|
|
158
|
+
// $dynamicRef with JS codegen: direct path, no wrapper layers
|
|
159
|
+
const _fn = jsFn, _efn = errOnly, _R = VALID_RESULT;
|
|
160
|
+
this.validate = preprocess
|
|
161
|
+
? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
|
|
162
|
+
: (data) => _fn(data) ? _R : _efn(data);
|
|
163
|
+
} else if (hasDynRef) {
|
|
164
|
+
// $dynamicRef without codegen: the interpreted engine. It scores the
|
|
165
|
+
// same on the suite's $dynamicRef cases as the native walker since the
|
|
166
|
+
// dynamic-scope fix, needs no addon, and gets the verdict-only mode.
|
|
167
|
+
if (!_interp) {
|
|
168
|
+
const { createInterpreter } = require('./lib/interpreter');
|
|
169
|
+
_interp = createInterpreter(schemaObj, {
|
|
170
|
+
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
171
|
+
formats: this._userFormats,
|
|
172
|
+
v1: isV1Dialect(schemaObj),
|
|
173
|
+
keywords: this._keywords,
|
|
131
174
|
});
|
|
175
|
+
}
|
|
176
|
+
const interp = _interp;
|
|
177
|
+
this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
|
|
178
|
+
this.validate = preprocess
|
|
179
|
+
? (data) => { preprocess(data); return interp.validate(data); }
|
|
180
|
+
: (data) => interp.validate(data);
|
|
181
|
+
} else if (jsFn && jsFn._hybridFactory) {
|
|
182
|
+
// Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
|
|
183
|
+
// No arrow function wrapper, no ternary, one function call
|
|
184
|
+
// The factory bakes the error function in as an argument and never
|
|
185
|
+
// calls it for a document that passes, so a resolver here costs the
|
|
186
|
+
// accepted path nothing and keeps the compile off the first call.
|
|
187
|
+
// Until the first rejection this is the hybrid: the verdict function,
|
|
188
|
+
// with the error resolver baked in and never called for a document that
|
|
189
|
+
// passes. That keeps the combined function's compile off the first call,
|
|
190
|
+
// which is two thirds of what a first call costs.
|
|
191
|
+
//
|
|
192
|
+
// From the first rejection on, the combined function answers directly.
|
|
193
|
+
// It decides and collects in one pass, so the verdict pass in front of it
|
|
194
|
+
// was validating the document a second time: 134.1 microseconds against
|
|
195
|
+
// its 45.3 on a 1000-user array. Swapping rather than starting there keeps
|
|
196
|
+
// the lazy compile, and swapping at all is only free because the combined
|
|
197
|
+
// function now costs what the verdict function costs on accepted
|
|
198
|
+
// documents (1.02x on that array, 0.99x on a small body) since
|
|
199
|
+
// additionalProperties stopped materialising its key array. The
|
|
200
|
+
// indirection this needs measured inside the noise at both sizes.
|
|
201
|
+
let impl = null;
|
|
202
|
+
const onReject = (data) => {
|
|
203
|
+
const combined = combinedIfSafe();
|
|
204
|
+
if (combined) { impl = combined; return _mustReject(combined(data)); }
|
|
205
|
+
return errOnly(data);
|
|
206
|
+
};
|
|
207
|
+
// Tiered: the first calls go through the verdict function and the
|
|
208
|
+
// resolver, and the hybrid is compiled once the validator is in use.
|
|
209
|
+
// Compiling it up front was a second parse of the whole schema on
|
|
210
|
+
// every validator, most of which answer a handful of requests or only
|
|
211
|
+
// verdicts. A refused compile stays on the first tier.
|
|
212
|
+
let warm = 0;
|
|
213
|
+
const first = (data) => {
|
|
214
|
+
if (++warm >= HYBRID_TIER_CALLS && impl === first) {
|
|
215
|
+
impl = jsFn._hybridFactory(VALID_RESULT, onReject) || ((d) => (jsFn(d) ? VALID_RESULT : onReject(d)));
|
|
216
|
+
return impl(data);
|
|
217
|
+
}
|
|
218
|
+
return jsFn(data) ? VALID_RESULT : onReject(data);
|
|
219
|
+
};
|
|
220
|
+
impl = first;
|
|
221
|
+
const run = (data) => impl(data);
|
|
222
|
+
this.validate = preprocess
|
|
223
|
+
? (data) => { preprocess(data); return run(data); }
|
|
224
|
+
: run;
|
|
225
|
+
} else {
|
|
226
|
+
// No hybrid factory, so the assembly needs the function itself rather
|
|
227
|
+
// than a reference it can call later: build it now.
|
|
228
|
+
const safeCombinedFn = combinedIfSafe();
|
|
229
|
+
if (safeCombinedFn) {
|
|
230
|
+
this.validate = preprocess
|
|
231
|
+
? (data) => { preprocess(data); return safeCombinedFn(data); }
|
|
232
|
+
: safeCombinedFn;
|
|
132
233
|
} else {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
234
|
+
this.validate = preprocess
|
|
235
|
+
? (data) => {
|
|
236
|
+
preprocess(data);
|
|
237
|
+
return jsFn(data) ? VALID_RESULT : errOnly(data);
|
|
238
|
+
}
|
|
239
|
+
: (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Verbose mode: populate parentSchema, schema and data on each error, the
|
|
243
|
+
// three fields the default error shape carries under the same option.
|
|
244
|
+
// `data` is the value the error points at; without it a caller has to
|
|
245
|
+
// walk the document by the instance path itself, which is what one
|
|
246
|
+
// migration ended up writing by hand. Errors may be frozen, so clone
|
|
247
|
+
// them with the extra fields.
|
|
248
|
+
if (this._verbose) {
|
|
249
|
+
const inner = this.validate;
|
|
250
|
+
const root = this._schemaObj;
|
|
251
|
+
const { resolvePointer } = require('./lib/pointer.js');
|
|
252
|
+
this.validate = (data) => {
|
|
253
|
+
const result = inner(data);
|
|
254
|
+
if (result && !result.valid && result.errors) {
|
|
255
|
+
const enriched = result.errors.map((err) => {
|
|
256
|
+
if (!err || err.parentSchema !== undefined) return err;
|
|
257
|
+
const parentSchema = resolveSchemaByPath(root, err.schemaPath);
|
|
258
|
+
// The last segment of the schema path is the keyword that
|
|
259
|
+
// failed, so its value on the parent is that keyword's schema.
|
|
260
|
+
const sp = typeof err.schemaPath === 'string' ? err.schemaPath : '';
|
|
261
|
+
const last = sp.slice(sp.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
262
|
+
const keywordSchema = (parentSchema !== null && typeof parentSchema === 'object' && last)
|
|
263
|
+
? parentSchema[last]
|
|
264
|
+
: undefined;
|
|
265
|
+
return {
|
|
266
|
+
...err,
|
|
267
|
+
parentSchema,
|
|
268
|
+
schema: keywordSchema,
|
|
269
|
+
data: resolvePointer(data, err.instancePath, undefined),
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
return { valid: false, errors: enriched };
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// The verdict methods answer validate()'s question without building the
|
|
278
|
+
// error list, so they run the same preprocess pass. Skipping it made the
|
|
279
|
+
// two disagree on input that coercion or a default would have fixed.
|
|
280
|
+
_bindVerdict(this, fusedRemove
|
|
281
|
+
? (data) => fusedRemove(data) || (preprocess(data), jsFn(data))
|
|
282
|
+
: preprocess
|
|
283
|
+
? (data) => { preprocess(data); return jsFn(data) }
|
|
284
|
+
: jsFn);
|
|
285
|
+
// Same preference as the object path: the combined function first, since
|
|
286
|
+
// it validates and collects in one pass, and the error generator behind
|
|
287
|
+
// it. `errPreferCombined` is that order, resolved on the first rejection
|
|
288
|
+
// instead of at compile time.
|
|
289
|
+
// Tiered the same way as validate() above.
|
|
290
|
+
let jsonHybrid = null;
|
|
291
|
+
let jsonWarm = 0;
|
|
292
|
+
const jsonValidateTiered = (obj) => {
|
|
293
|
+
if (jsonHybrid !== null) return jsonHybrid(obj);
|
|
294
|
+
if (++jsonWarm >= HYBRID_TIER_CALLS) {
|
|
295
|
+
jsonHybrid = (jsFn._hybridFactory && jsFn._hybridFactory(VALID_RESULT, errPreferCombined))
|
|
296
|
+
|| ((o) => (jsFn(o) ? VALID_RESULT : errPreferCombined(o)));
|
|
297
|
+
return jsonHybrid(obj);
|
|
298
|
+
}
|
|
299
|
+
return jsFn(obj) ? VALID_RESULT : errPreferCombined(obj);
|
|
300
|
+
};
|
|
301
|
+
// Parsed text takes the same preprocess pass as a parsed object, so
|
|
302
|
+
// validate(obj) and validateJSON(text) answer the same for the same
|
|
303
|
+
// document. Without it, coercion, removal and defaults applied on one
|
|
304
|
+
// path and not the other.
|
|
305
|
+
// abortEarly holds for the text path too: the stub, not errors, as the
|
|
306
|
+
// scanner below and the interpreted engine already answered.
|
|
307
|
+
const jsonValidateInner = options.abortEarly
|
|
308
|
+
? (obj) => (jsFn(obj) ? VALID_RESULT : ABORT_EARLY_RESULT)
|
|
309
|
+
: jsonValidateTiered;
|
|
310
|
+
const jsonValidateFn = preprocess
|
|
311
|
+
? (obj) => { preprocess(obj); return jsonValidateInner(obj) }
|
|
312
|
+
: jsonValidateInner;
|
|
313
|
+
// A document at or above the simdjson threshold is answered by the native
|
|
314
|
+
// walker without being parsed, except for the shapes lib/buffer-gate.js
|
|
315
|
+
// lists, where the walker disagrees with validate(); those schemas never
|
|
316
|
+
// ask it. A rejection's errors come from the same path as a small
|
|
317
|
+
// document's, on first read. The addon's own validateJSON used to supply
|
|
318
|
+
// them and accepted documents validate() rejects, 231 of the official
|
|
319
|
+
// suite's cases once padded past the threshold.
|
|
320
|
+
let nativeText; // undefined until the first large document
|
|
321
|
+
const nativeVerdict = (jsonStr) => {
|
|
322
|
+
if (nativeText === undefined) {
|
|
323
|
+
nativeText = !!getNative() && !require('./lib/buffer-gate.js').bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords);
|
|
324
|
+
if (nativeText) {
|
|
325
|
+
this._ensureNative();
|
|
326
|
+
if (!(this._fastSlot >= 0)) nativeText = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return nativeText ? getNative().rawFastValidate(this._fastSlot, Buffer.from(jsonStr)) : undefined;
|
|
330
|
+
};
|
|
331
|
+
const validateText = (jsonStr) => {
|
|
332
|
+
let obj;
|
|
333
|
+
try {
|
|
334
|
+
obj = JSON.parse(jsonStr);
|
|
335
|
+
} catch (e) {
|
|
336
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
337
|
+
return _jsonSyntaxRejection(e);
|
|
338
|
+
}
|
|
339
|
+
return jsonValidateFn(obj);
|
|
340
|
+
};
|
|
341
|
+
this.validateJSON = useSimdjsonForLarge && !preprocess
|
|
342
|
+
? (jsonStr) => {
|
|
343
|
+
// `_skipNativeFast` is set by the scanner short-circuit below when it
|
|
344
|
+
// has already decided the document is invalid. The encode and the
|
|
345
|
+
// native call would run only to return false.
|
|
346
|
+
if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
|
|
347
|
+
const ok = nativeVerdict(jsonStr);
|
|
348
|
+
if (ok === true) return VALID_RESULT;
|
|
349
|
+
if (ok === false) return options.abortEarly ? ABORT_EARLY_RESULT : new TextRejection(jsonStr, validateText);
|
|
143
350
|
}
|
|
144
|
-
|
|
351
|
+
return validateText(jsonStr);
|
|
352
|
+
}
|
|
353
|
+
: validateText;
|
|
354
|
+
// The addon validates the bytes as they are, which is the wrong answer
|
|
355
|
+
// when the schema asks for coercion, removal or defaults: those change
|
|
356
|
+
// what counts as valid. With a preprocess pass configured the text is
|
|
357
|
+
// parsed and run through the same path validate() takes.
|
|
358
|
+
const verdictFromText = (jsonStr) => {
|
|
359
|
+
let parsed;
|
|
360
|
+
try {
|
|
361
|
+
parsed = JSON.parse(jsonStr);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
364
|
+
return false;
|
|
145
365
|
}
|
|
366
|
+
if (preprocess) preprocess(parsed);
|
|
367
|
+
return jsFn(parsed);
|
|
368
|
+
};
|
|
369
|
+
this.isValidJSON = useSimdjsonForLarge && !preprocess
|
|
370
|
+
? (jsonStr) => {
|
|
371
|
+
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
372
|
+
const ok = nativeVerdict(jsonStr);
|
|
373
|
+
if (ok !== undefined) return ok;
|
|
374
|
+
}
|
|
375
|
+
return verdictFromText(jsonStr);
|
|
376
|
+
}
|
|
377
|
+
: verdictFromText;
|
|
378
|
+
|
|
379
|
+
// A schema-directed scanner answers the verdict from the JSON text
|
|
380
|
+
// without building the document. Parsing is around three quarters of the
|
|
381
|
+
// cost of a real request, and a caller that only wants yes or no should
|
|
382
|
+
// not pay it; a rejection can also stop at the byte that caused it
|
|
383
|
+
// instead of parsing the rest of a document that is already refused.
|
|
384
|
+
//
|
|
385
|
+
// It is wired only where the verdict IS the answer. On a path that has
|
|
386
|
+
// to produce errors, scanning an invalid document is work thrown away,
|
|
387
|
+
// so those keep parsing. `abortEarly` has no errors to produce, so it
|
|
388
|
+
// counts as a verdict path.
|
|
389
|
+
//
|
|
390
|
+
// Not wired when a preprocess pass is configured: coercion, removal and
|
|
391
|
+
// defaults rewrite the document before it is judged, and the scanner
|
|
392
|
+
// reads what arrived. The compiler declines any schema it cannot answer
|
|
393
|
+
// and a compiled scanner returns BAIL for a document shape it cannot
|
|
394
|
+
// answer, and then the parse path below takes over unchanged.
|
|
395
|
+
};
|
|
146
396
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
397
|
+
// The verdict function alone, for isValidObject() before a full compile: the
|
|
398
|
+
// error and combined generators are left for the first rejection.
|
|
399
|
+
installCodegenPaths.compileVerdict = function compileVerdict () {
|
|
400
|
+
const { compileCacheKey, _compileCache, _bindVerdict, _rememberInstance } = core._internals;
|
|
401
|
+
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
402
|
+
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
403
|
+
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
404
|
+
// Custom formats are JS functions: skip the shared cache so different
|
|
405
|
+
// validators with the same schema string but different formats don't collide.
|
|
406
|
+
const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
|
|
407
|
+
if (cached && cached.jsFn) {
|
|
408
|
+
this._jsFn = cached.jsFn;
|
|
409
|
+
_bindVerdict(this, cached.jsFn);
|
|
410
|
+
_rememberInstance(this);
|
|
411
|
+
return;
|
|
151
412
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (typeof v === "boolean") return v ? 1 : 0;
|
|
171
|
-
};
|
|
172
|
-
case "string":
|
|
173
|
-
return (v) => {
|
|
174
|
-
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
175
|
-
};
|
|
176
|
-
case "boolean":
|
|
177
|
-
return (v) => {
|
|
178
|
-
if (v === "true" || v === "1") return true;
|
|
179
|
-
if (v === "false" || v === "0") return false;
|
|
180
|
-
};
|
|
181
|
-
default:
|
|
182
|
-
return null;
|
|
413
|
+
const uf = this._userFormats;
|
|
414
|
+
const _cg = compileToJSCodegen(this._schemaObj, sm, uf);
|
|
415
|
+
const jsFn = _cg || compileToJS(this._schemaObj, null, sm);
|
|
416
|
+
this._jsFn = jsFn;
|
|
417
|
+
if (jsFn) {
|
|
418
|
+
_bindVerdict(this, jsFn);
|
|
419
|
+
_rememberInstance(this);
|
|
420
|
+
// A partial entry: the verdict function is real, the other two are not
|
|
421
|
+
// built yet rather than declined. `undefined` is the not-built marker
|
|
422
|
+
// the full compile's _buildErr/_buildCombined look for; `null` would read as
|
|
423
|
+
// "the compiler declined" and cost the schema its error function, which
|
|
424
|
+
// is the bug this cache had once already. `isCodegen` rides along so a
|
|
425
|
+
// validator that later reuses this entry reports the same engine it
|
|
426
|
+
// would have compiled to.
|
|
427
|
+
if (!uf) {
|
|
428
|
+
if (!cached) _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
|
|
429
|
+
else cached.jsFn = jsFn;
|
|
430
|
+
}
|
|
183
431
|
}
|
|
184
|
-
}
|
|
432
|
+
};
|
|
185
433
|
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
434
|
+
// validateJSON and isValidJSON over a schema-directed scanner, which answers
|
|
435
|
+
// from the JSON text without building the document.
|
|
436
|
+
installCodegenPaths.installScanner = function installScanner (schemaObj, options) {
|
|
437
|
+
const { ABORT_EARLY_RESULT, VALID_RESULT, _bindEntry } = core._internals;
|
|
438
|
+
const self = this;
|
|
439
|
+
// Generating a scanner costs about 20 microseconds, measured, and it
|
|
440
|
+
// saves from around 85 nanoseconds on a small accepted document to
|
|
441
|
+
// several microseconds on a rejected one. Building it on the first
|
|
442
|
+
// call would therefore be a straight loss for a caller that checks one
|
|
443
|
+
// document and exits, so it is built once a caller has asked often
|
|
444
|
+
// enough that it is plainly doing this in a loop. A server passes the
|
|
445
|
+
// line during warm-up and never sees it.
|
|
446
|
+
const SCAN_AFTER = 64;
|
|
447
|
+
let calls = 0;
|
|
448
|
+
// undefined: not built. null: this schema has no scanner. Passing true
|
|
449
|
+
// builds it now, which is how the differential test reaches it.
|
|
450
|
+
this._ensureScanner = (now) => {
|
|
451
|
+
if (self._scanner === undefined) {
|
|
452
|
+
if (!now && ++calls < SCAN_AFTER) return undefined;
|
|
453
|
+
const built = require('./lib/scan-compiler').compileScanner(schemaObj, { userFormats: self._userFormats });
|
|
454
|
+
self._scanner = built ? built.scan : null;
|
|
455
|
+
}
|
|
456
|
+
return self._scanner;
|
|
195
457
|
};
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
target = target[parentPath[j]];
|
|
458
|
+
const byParsing = this.isValidJSON;
|
|
459
|
+
// The verdict is a pure function of the text, and the caller a
|
|
460
|
+
// gateway or a drift monitor keeps asking about is usually the same
|
|
461
|
+
// text: a config file re-read on a timer, a heartbeat body. One
|
|
462
|
+
// remembered (text, verdict) pair answers that case with a native
|
|
463
|
+
// string compare, which is a memcmp, instead of a scan. Withheld when
|
|
464
|
+
// user formats or custom keywords are present, since those are user
|
|
465
|
+
// functions and nothing guarantees they are pure.
|
|
466
|
+
const memoizable = !self._userFormats && !self._usesKeywords;
|
|
467
|
+
let _memoText = null;
|
|
468
|
+
let _memoVerdict = false;
|
|
469
|
+
this.isValidJSON = (jsonStr) => {
|
|
470
|
+
const scan = self._ensureScanner();
|
|
471
|
+
if (scan === undefined) return byParsing(jsonStr);
|
|
472
|
+
if (scan === null) { _bindEntry(self, 'isValidJSON', byParsing); return byParsing(jsonStr); }
|
|
473
|
+
_bindEntry(self, 'isValidJSON', memoizable
|
|
474
|
+
? (text) => {
|
|
475
|
+
if (typeof text !== 'string') return byParsing(text);
|
|
476
|
+
if (text === _memoText) return _memoVerdict;
|
|
477
|
+
const r = scan(text);
|
|
478
|
+
const verdict = r === -1 ? byParsing(text) : r === 1;
|
|
479
|
+
_memoText = text;
|
|
480
|
+
_memoVerdict = verdict;
|
|
481
|
+
return verdict;
|
|
221
482
|
}
|
|
222
|
-
|
|
223
|
-
typeof
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
483
|
+
: (text) => {
|
|
484
|
+
if (typeof text !== 'string') return byParsing(text);
|
|
485
|
+
const r = scan(text);
|
|
486
|
+
if (r === -1) return byParsing(text);
|
|
487
|
+
return r === 1;
|
|
488
|
+
});
|
|
489
|
+
return self.isValidJSON(jsonStr);
|
|
490
|
+
};
|
|
491
|
+
// validateJSON gets the same short-circuit isValidJSON has. The verdict is
|
|
492
|
+
// a property of the text, and the scanner reads the text once and
|
|
493
|
+
// allocates nothing; above the simdjson threshold the path underneath
|
|
494
|
+
// encoded the whole document to a Buffer and called the native validator
|
|
495
|
+
// instead, which measured 340 microseconds against the scanner's 191 on a
|
|
496
|
+
// 149 KB config, the same against the published addon as against a local
|
|
497
|
+
// build.
|
|
498
|
+
//
|
|
499
|
+
// An accepted document stops at the scanner. A rejected one still has to
|
|
500
|
+
// produce errors, so it goes on to the path below, which is the
|
|
501
|
+
// rich-errors wrapper and everything under it; the flag only tells that
|
|
502
|
+
// path to skip an encode and a native call that would return false. Doing
|
|
503
|
+
// it the other way, returning errors from the inner function directly,
|
|
504
|
+
// would hand back errors that never passed through enrichment.
|
|
505
|
+
{
|
|
506
|
+
const validateByParsing = this.validateJSON;
|
|
507
|
+
const abortEarly = !!options.abortEarly;
|
|
508
|
+
this.validateJSON = (jsonStr) => {
|
|
509
|
+
const scan = self._ensureScanner();
|
|
510
|
+
if (scan === undefined) return validateByParsing(jsonStr);
|
|
511
|
+
if (scan === null) { _bindEntry(self, 'validateJSON', validateByParsing); return validateByParsing(jsonStr); }
|
|
512
|
+
_bindEntry(self, 'validateJSON', (text) => {
|
|
513
|
+
if (typeof text === 'string') {
|
|
514
|
+
const r = scan(text);
|
|
515
|
+
if (r === 1) return VALID_RESULT;
|
|
516
|
+
if (r === 0) {
|
|
517
|
+
if (abortEarly) return ABORT_EARLY_RESULT;
|
|
518
|
+
self._skipNativeFast = true;
|
|
519
|
+
try {
|
|
520
|
+
return validateByParsing(text);
|
|
521
|
+
} finally {
|
|
522
|
+
self._skipNativeFast = false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
231
525
|
}
|
|
526
|
+
return validateByParsing(text);
|
|
232
527
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Always recurse into nested properties (they may have their own additionalProperties: false)
|
|
237
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
238
|
-
if (prop && typeof prop === "object" && prop.properties) {
|
|
239
|
-
collectRemovals(prop, actions, (path || []).concat(key));
|
|
240
|
-
}
|
|
528
|
+
return self.validateJSON(jsonStr);
|
|
529
|
+
};
|
|
241
530
|
}
|
|
242
|
-
}
|
|
531
|
+
};
|
|
243
532
|
|
|
533
|
+
// The defaults, coercion and removal pass as generated source, which runs
|
|
534
|
+
// about twelve times faster than the closure mutators in validator-core.js.
|
|
535
|
+
// ata-validator/lite takes the closures.
|
|
244
536
|
// Emit the in-place strip for one schema node and everything under its
|
|
245
537
|
// `properties`. Scope matches collectRemovals(): object properties only, so
|
|
246
538
|
// the two paths keep the same answer.
|
|
@@ -281,7 +573,6 @@ function buildPreprocessCodegen(schema, options) {
|
|
|
281
573
|
if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
|
|
282
574
|
const lines = [];
|
|
283
575
|
const props = schema.properties;
|
|
284
|
-
const keys = Object.keys(props);
|
|
285
576
|
|
|
286
577
|
// removeAdditional: strip unknown keys at every level the schema describes,
|
|
287
578
|
// not just the top one. The closure path below (collectRemovals) always
|
|
@@ -347,2794 +638,118 @@ function buildPreprocessCodegen(schema, options) {
|
|
|
347
638
|
}
|
|
348
639
|
}
|
|
349
640
|
|
|
350
|
-
// Cloudflare Workers, Deno Deploy and pages under a strict Content-Security-
|
|
351
|
-
// Policy refuse `new Function`. Probed once, lazily, because the answer cannot
|
|
352
|
-
// change within a realm and the probe itself is a code generation attempt.
|
|
353
|
-
let _codegenAvailable = null;
|
|
354
|
-
function codegenAvailable() {
|
|
355
|
-
if (_codegenAvailable === null) {
|
|
356
|
-
try {
|
|
357
|
-
_codegenAvailable = new Function('return 1')() === 1;
|
|
358
|
-
} catch {
|
|
359
|
-
_codegenAvailable = false;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return _codegenAvailable;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Schema compilation cache: same schema string -> reuse compiled functions
|
|
366
|
-
const _compileCache = new Map();
|
|
367
|
-
|
|
368
|
-
// Object identity cache: same schema object reference -> reuse entire compiled state
|
|
369
|
-
// Skips JSON.stringify, cache lookup, and all setup. Near-zero cost for repeated schemas.
|
|
370
|
-
const _identityCache = new WeakMap();
|
|
371
|
-
|
|
372
|
-
const SIMDJSON_PADDING = 64;
|
|
373
|
-
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
374
|
-
// How many calls a validator answers through its verdict function before its
|
|
375
|
-
// validate() and validateJSON() compile the single-function hybrid.
|
|
376
|
-
const HYBRID_TIER_CALLS = 64;
|
|
377
|
-
const ABORT_EARLY_RESULT = Object.freeze({
|
|
378
|
-
valid: false,
|
|
379
|
-
errors: Object.freeze([Object.freeze({
|
|
380
|
-
code: 'ATA9000',
|
|
381
|
-
message: 'validation failed',
|
|
382
|
-
keyword: '__abort_early__',
|
|
383
|
-
path: '',
|
|
384
|
-
})]),
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// `_CP_LEN_SOURCE`, the safe-regex embed, and the AOT helpers that consume them
|
|
388
|
-
// now live in `lib/aot.js` — keeping this file free of `fs`/`path`/`__dirname`
|
|
389
|
-
// references so a default import never touches disk. The static AOT methods
|
|
390
|
-
// further down lazily require `./lib/aot`, so they pay nothing until a user
|
|
391
|
-
// calls `bundleStandalone`/`bundle`/etc.
|
|
392
|
-
|
|
393
|
-
// Above this size, simdjson On Demand (selective field access) beats JSON.parse
|
|
394
|
-
// (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
|
|
395
|
-
|
|
396
|
-
// Rejection result with errors materialized on first read. The accessor
|
|
397
|
-
// lives on the prototype so constructing one is a plain allocation; an
|
|
398
|
-
// object-literal getter would create a closure and define an accessor
|
|
399
|
-
// property on every rejection, which showed up as the single largest cost
|
|
400
|
-
// on the rejection path. `toJSON` keeps JSON.stringify output identical to
|
|
401
|
-
// the eager shape. Note for tests: deepStrictEqual against a plain object
|
|
402
|
-
// compares prototypes; read `.errors` and compare that.
|
|
403
|
-
class LazyRejection {
|
|
404
|
-
constructor(build, data, buildRaw) {
|
|
405
|
-
this.valid = false;
|
|
406
|
-
this._build = build;
|
|
407
|
-
this._data = data;
|
|
408
|
-
this._errors = null;
|
|
409
|
-
this._buildRaw = buildRaw;
|
|
410
|
-
}
|
|
411
|
-
toJSON() {
|
|
412
|
-
return { valid: false, errors: this.errors };
|
|
413
|
-
}
|
|
414
|
-
// Raw shape for consumers that carry only message and path, such as the
|
|
415
|
-
// Standard Schema bridge: schema order, no enrichment. Reading `errors`
|
|
416
|
-
// afterwards still enriches through its own build.
|
|
417
|
-
_ataRaw() {
|
|
418
|
-
return this._buildRaw ? this._buildRaw(this._data) : this.errors;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
Object.defineProperty(LazyRejection.prototype, 'errors', {
|
|
422
|
-
enumerable: true,
|
|
423
|
-
configurable: true,
|
|
424
|
-
get() {
|
|
425
|
-
if (this._errors === null) this._errors = this._build(this._data);
|
|
426
|
-
return this._errors;
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const SIMDJSON_THRESHOLD = 8192;
|
|
431
|
-
|
|
432
|
-
// Resolve a JSON Schema path like "#/properties/name/type" to the schema object
|
|
433
|
-
// that *contains* the failing keyword. Used by verbose mode to populate
|
|
434
|
-
// `parentSchema` on validation errors. Returns undefined if the path can't be
|
|
435
|
-
// walked (malformed pointer or missing intermediate node).
|
|
436
|
-
function resolveSchemaByPath(rootSchema, schemaPath) {
|
|
437
|
-
if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) {
|
|
438
|
-
return undefined;
|
|
439
|
-
}
|
|
440
|
-
const stripped = schemaPath.slice(1);
|
|
441
|
-
if (!stripped || stripped === '/') return rootSchema;
|
|
442
|
-
const parts = stripped.split('/').filter(Boolean).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
443
|
-
// The last segment is the keyword that failed (e.g. "type"); parentSchema is
|
|
444
|
-
// the schema object that owns that keyword, so walk all but the last segment.
|
|
445
|
-
let target = rootSchema;
|
|
446
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
447
|
-
if (target == null || typeof target !== 'object') return undefined;
|
|
448
|
-
target = target[parts[i]];
|
|
449
|
-
}
|
|
450
|
-
return target;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Rank an error by walking its schemaPath through the schema object: at each
|
|
454
|
-
// level the segment's index among the node's declared keys. Comparing ranks
|
|
455
|
-
// lexicographically orders errors by keyword declaration order, which is the
|
|
456
|
-
// order AJV emits and what schema authors read top to bottom. Segments that
|
|
457
|
-
// cannot be resolved (cross-schema refs, normalized keys) end the walk; the
|
|
458
|
-
// stable sort then keeps such errors in engine emission order.
|
|
459
|
-
// The rank of a `schemaPath` under a given root is fixed: the schema does not
|
|
460
|
-
// change between validations, so neither does the answer. It was recomputed for
|
|
461
|
-
// every error of every failing document, and computing it is not cheap. Two
|
|
462
|
-
// caches, both keyed on things that do not change:
|
|
463
|
-
//
|
|
464
|
-
// rootSchema -> schemaPath -> rank, so a path is walked once ever
|
|
465
|
-
// node -> key -> its index, so the walk stops calling Object.keys and
|
|
466
|
-
// scanning the result for a string
|
|
467
|
-
//
|
|
468
|
-
// A failing route sees the same handful of schemaPaths over and over, which is
|
|
469
|
-
// what makes the first one worth having.
|
|
470
|
-
const _rankCache = new WeakMap();
|
|
471
|
-
const { rankFor: schemaOrderRank, ordinalFor: schemaOrdinal } = require('./lib/schema-order');
|
|
472
641
|
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
this._result = result;
|
|
488
|
-
this._data = data;
|
|
489
|
-
this._rawInput = rawInput;
|
|
490
|
-
this._self = self;
|
|
491
|
-
this._root = root;
|
|
492
|
-
this._enrich = enrich;
|
|
493
|
-
this._cached = null;
|
|
494
|
-
}
|
|
495
|
-
toJSON() {
|
|
496
|
-
return { valid: false, errors: this.errors };
|
|
497
|
-
}
|
|
498
|
-
_ataRaw() {
|
|
499
|
-
let raw = this._result.errors || [];
|
|
500
|
-
if (raw.length > 1) raw = sortErrorsBySchemaOrder(this._root, raw);
|
|
501
|
-
return raw;
|
|
642
|
+
// parse() for the full package: a generated function that copies the keys the
|
|
643
|
+
// schema declares, behind the generated verdict. The refusals every build
|
|
644
|
+
// shares stay in validator-core.js; this is the part that needs the code
|
|
645
|
+
// generator.
|
|
646
|
+
function buildParse (self, decline, extended) {
|
|
647
|
+
const { cloneExprFor } = require('./lib/clone-emit');
|
|
648
|
+
const expr = cloneExprFor(self._schemaObj);
|
|
649
|
+
if (!expr) return decline('the set of keys to keep cannot be proven from the schema');
|
|
650
|
+
let copy;
|
|
651
|
+
try {
|
|
652
|
+
// eslint-disable-next-line no-new-func
|
|
653
|
+
copy = new Function('data', 'return ' + expr);
|
|
654
|
+
} catch {
|
|
655
|
+
return decline('code generation is not allowed here');
|
|
502
656
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// The position map, resolved now that an error is actually being read.
|
|
514
|
-
let positions = null;
|
|
515
|
-
if (enrich && raw.length && this._rawInput != null) {
|
|
516
|
-
positions = self._pos().targeted(this._rawInput, wantedPointersFor(raw));
|
|
517
|
-
if (positions) self._posCache.reset();
|
|
657
|
+
self._ensureCompiled();
|
|
658
|
+
const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
|
|
659
|
+
if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
|
|
660
|
+
return (data) => {
|
|
661
|
+
if (!verdict(data)) {
|
|
662
|
+
const e = new Error('validation failed');
|
|
663
|
+
e.name = 'AtaValidationError';
|
|
664
|
+
let target = data;
|
|
665
|
+
if (self._mutatesInput) {
|
|
666
|
+
try { target = structuredClone(data); } catch { target = data; }
|
|
518
667
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
? {
|
|
522
|
-
data: this._data,
|
|
523
|
-
positions,
|
|
524
|
-
schemaPositions: self._schemaPositions,
|
|
525
|
-
schemaFile: self._source ? self._source.path : undefined,
|
|
526
|
-
}
|
|
527
|
-
: null;
|
|
528
|
-
const cached = opts
|
|
529
|
-
? raw.map((e) => enrich(e, opts))
|
|
530
|
-
// The v0.14 shape is a fixed key set; the ordering key the
|
|
531
|
-
// generated code carries is dropped from it here.
|
|
532
|
-
: raw.map(stripOrdinal);
|
|
533
|
-
// Correlation is published, never applied. Both halves of a typo pair
|
|
534
|
-
// stay in the array; `related` only says they are one mistake, so a
|
|
535
|
-
// wrong pairing costs a sentence rather than a hidden violation.
|
|
536
|
-
if (enrich && cached.length > 1) attachRelated(cached);
|
|
537
|
-
// No diagnostic payload here. validate(data) is the library hot path,
|
|
538
|
-
// and attaching one cost about 100 ns per rejection for a consumer
|
|
539
|
-
// that never renders. The text path attaches it, and a renderer given
|
|
540
|
-
// `{ data }` builds frames for object input on request.
|
|
541
|
-
this._cached = cached;
|
|
668
|
+
e.errors = self.validate(target).errors;
|
|
669
|
+
throw e;
|
|
542
670
|
}
|
|
543
|
-
return
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
// The rejection validateJSON returns. Everything the text path adds over
|
|
548
|
-
// validate(data), the value tree enrichment needs, the position map, the
|
|
549
|
-
// diagnostic payload, happens on first access to `.errors`. Reading `.valid`
|
|
550
|
-
// touches none of it. The JSON text is held here rather than read back off the
|
|
551
|
-
// validator, so the result does not depend on what the instance does next.
|
|
552
|
-
class LazyJsonRejection {
|
|
553
|
-
constructor(result, jsonStr, self, enrich) {
|
|
554
|
-
this.valid = false;
|
|
555
|
-
this._result = result;
|
|
556
|
-
this._jsonStr = jsonStr;
|
|
557
|
-
this._self = self;
|
|
558
|
-
this._enrich = enrich;
|
|
559
|
-
this._cached = null;
|
|
560
|
-
}
|
|
561
|
-
toJSON() {
|
|
562
|
-
return { valid: false, errors: this.errors };
|
|
563
|
-
}
|
|
671
|
+
return copy(data);
|
|
672
|
+
};
|
|
564
673
|
}
|
|
565
|
-
Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
566
|
-
enumerable: true,
|
|
567
|
-
configurable: true,
|
|
568
|
-
get() {
|
|
569
|
-
if (this._cached !== null) return this._cached;
|
|
570
|
-
const self = this._self;
|
|
571
|
-
const enrich = this._enrich;
|
|
572
|
-
const jsonStr = this._jsonStr;
|
|
573
|
-
// Reading the inner errors realizes the inner lazy layer, if there was one.
|
|
574
|
-
const raw = this._result.errors || [];
|
|
575
|
-
if (!raw.length) { this._cached = raw; return raw; }
|
|
576
|
-
|
|
577
|
-
// The enrich pass plucks `received` from the value tree and the suggestion
|
|
578
|
-
// engine (required-typo, format hints, coercion nudges) walks it too.
|
|
579
|
-
let parsedData;
|
|
580
|
-
try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
|
|
581
|
-
|
|
582
|
-
// Errors the inner path already enriched carry a docUrl: only enrich() sets
|
|
583
|
-
// one. `code` is not a safe signal, because branch-collapse attaches codes
|
|
584
|
-
// to raw errors, and detecting on it left every collapsed oneOf/anyOf error
|
|
585
|
-
// unenriched on the text path.
|
|
586
|
-
if (!raw[0] || !raw[0].docUrl) {
|
|
587
|
-
const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
|
|
588
|
-
// Declaration order, as validate() applies it; the text path used to
|
|
589
|
-
// enrich in emission order.
|
|
590
|
-
const ordered = raw.length > 1 ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
|
|
591
|
-
const enrichOpts = {
|
|
592
|
-
data: parsedData,
|
|
593
|
-
positions,
|
|
594
|
-
schemaPositions: self._schemaPositions,
|
|
595
|
-
schemaFile: self._source ? self._source.path : undefined,
|
|
596
|
-
};
|
|
597
|
-
const enriched = ordered.map((e) => enrich(e, enrichOpts));
|
|
598
|
-
if (enriched.length > 1) attachRelated(enriched);
|
|
599
|
-
attachDiagnosticSource(enriched, {
|
|
600
|
-
data: parsedData,
|
|
601
|
-
text: jsonStr,
|
|
602
|
-
positions,
|
|
603
|
-
schema: self._schemaObj,
|
|
604
|
-
mutatesInput: self._mutatesInput === true,
|
|
605
|
-
});
|
|
606
|
-
if (positions) self._posCache.reset();
|
|
607
|
-
this._cached = enriched;
|
|
608
|
-
return enriched;
|
|
609
|
-
}
|
|
610
674
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
self._posCache.reset();
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
if (raw.length > 1) attachRelated(raw);
|
|
628
|
-
attachDiagnosticSource(raw, {
|
|
629
|
-
data: parsedData,
|
|
630
|
-
text: jsonStr,
|
|
631
|
-
schema: self._schemaObj,
|
|
632
|
-
mutatesInput: self._mutatesInput === true,
|
|
633
|
-
});
|
|
634
|
-
this._cached = raw;
|
|
635
|
-
return raw;
|
|
636
|
-
},
|
|
675
|
+
// Everything that turns a schema into JavaScript source, registered with the
|
|
676
|
+
// core: the three code generators and the paths above, the generated
|
|
677
|
+
// preprocess pass, the JSON-text scanner, the parse() copy and the
|
|
678
|
+
// ahead-of-time bundle methods. parse() and the bundle methods load on first
|
|
679
|
+
// use.
|
|
680
|
+
core._registerCodegen({
|
|
681
|
+
jsCompiler,
|
|
682
|
+
installPaths: installCodegenPaths,
|
|
683
|
+
compileVerdict: installCodegenPaths.compileVerdict,
|
|
684
|
+
installScanner: installCodegenPaths.installScanner,
|
|
685
|
+
buildPreprocess: buildPreprocessCodegen,
|
|
686
|
+
buildParse,
|
|
687
|
+
aot: () => require('./lib/aot.js'),
|
|
637
688
|
});
|
|
638
689
|
|
|
639
|
-
// The
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const params = e.params;
|
|
653
|
-
const named = params && (params.additionalProperty || params.unevaluatedProperty);
|
|
654
|
-
if (typeof named === 'string') {
|
|
655
|
-
wanted.add(path + '/' + named);
|
|
656
|
-
if (named.indexOf('~') !== -1 || named.indexOf('/') !== -1) {
|
|
657
|
-
wanted.add(path + '/' + named.replace(/~/g, '~0').replace(/\//g, '~1'));
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
return wanted;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// A raw error without the `_o` ordering key, for the legacy error shape.
|
|
665
|
-
function stripOrdinal(e) {
|
|
666
|
-
if (e === null || typeof e !== 'object' || e._o === undefined) return e;
|
|
667
|
-
const out = {};
|
|
668
|
-
for (const k in e) if (k !== '_o') out[k] = e[k];
|
|
669
|
-
return out;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Errors in schema declaration order. Each error's key is its schemaPath's
|
|
673
|
-
// pre-order ordinal in the root schema: written into the literal by the code
|
|
674
|
-
// generator (`_o`), looked up once per path otherwise. Most rejections come
|
|
675
|
-
// out already ordered, and those return without sorting or allocating.
|
|
676
|
-
function sortErrorsBySchemaOrder(rootSchema, errors) {
|
|
677
|
-
const n = errors.length;
|
|
678
|
-
const keys = new Array(n);
|
|
679
|
-
let sorted = true;
|
|
680
|
-
let prev = -1;
|
|
681
|
-
for (let i = 0; i < n; i++) {
|
|
682
|
-
const e = errors[i];
|
|
683
|
-
let o = typeof e._o === 'number' ? e._o : schemaOrdinal(rootSchema, e.schemaPath);
|
|
684
|
-
// An error with no place in this document (an appended custom-keyword
|
|
685
|
-
// error, a path into another schema) stays next to the error before it,
|
|
686
|
-
// which is where the rank comparison left it too.
|
|
687
|
-
if (o === null) o = prev < 0 ? 0 : prev;
|
|
688
|
-
keys[i] = o;
|
|
689
|
-
if (o < prev) sorted = false;
|
|
690
|
-
prev = o;
|
|
691
|
-
}
|
|
692
|
-
if (sorted) return errors;
|
|
693
|
-
// Error lists are short. A stable insertion sort over the integer keys
|
|
694
|
-
// moves the errors in tandem with no comparator calls and no index array.
|
|
695
|
-
const out = errors.slice();
|
|
696
|
-
for (let i = 1; i < n; i++) {
|
|
697
|
-
const k = keys[i];
|
|
698
|
-
const e = out[i];
|
|
699
|
-
let j = i - 1;
|
|
700
|
-
while (j >= 0 && keys[j] > k) { keys[j + 1] = keys[j]; out[j + 1] = out[j]; j--; }
|
|
701
|
-
keys[j + 1] = k;
|
|
702
|
-
out[j + 1] = e;
|
|
703
|
-
}
|
|
704
|
-
return out;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
// Paths repeat across rejections of the same shape, so the parsed segment
|
|
709
|
-
// list is cached per path string. Entries are frozen: the same array is
|
|
710
|
-
// handed to every issue that names the path.
|
|
711
|
-
//
|
|
712
|
-
// The cache is bucketed by length and searched with ===, not keyed in a Map.
|
|
713
|
-
// A path with an array index in it is concatenated afresh by every rejection,
|
|
714
|
-
// and a Map has to hash each such string from scratch before it can look
|
|
715
|
-
// anything up; that hashing was 30% of a Standard Schema rejection with
|
|
716
|
-
// sixteen issues. Comparing against the few paths of the same length costs
|
|
717
|
-
// less. Both levels are bounded so a stream of array indexes cannot grow it
|
|
718
|
-
// without limit: a full bucket stops caching, too many lengths clear it.
|
|
719
|
-
const _pathBuckets = new Map();
|
|
720
|
-
const PATH_BUCKET_MAX = 32;
|
|
721
|
-
const PATH_LENGTHS_MAX = 256;
|
|
722
|
-
function parsePointerPath(path) {
|
|
723
|
-
if (!path) return EMPTY_PATH;
|
|
724
|
-
const n = path.length;
|
|
725
|
-
let bucket = _pathBuckets.get(n);
|
|
726
|
-
if (bucket !== undefined) {
|
|
727
|
-
for (let i = 0; i < bucket.length; i += 2) if (bucket[i] === path) return bucket[i + 1];
|
|
728
|
-
} else {
|
|
729
|
-
if (_pathBuckets.size >= PATH_LENGTHS_MAX) _pathBuckets.clear();
|
|
730
|
-
bucket = [];
|
|
731
|
-
_pathBuckets.set(n, bucket);
|
|
732
|
-
}
|
|
733
|
-
const segs = Object.freeze(parsePointerPathUncached(path));
|
|
734
|
-
if (bucket.length < PATH_BUCKET_MAX * 2) bucket.push(path, segs);
|
|
735
|
-
return segs;
|
|
736
|
-
}
|
|
737
|
-
const EMPTY_PATH = Object.freeze([]);
|
|
738
|
-
|
|
739
|
-
function parsePointerPathUncached(path) {
|
|
740
|
-
// One pass, no intermediate arrays. Per Standard Schema V1 an array index
|
|
741
|
-
// is emitted as a number and an object key as a string; a segment is an
|
|
742
|
-
// index when it is all digits with no leading zero.
|
|
743
|
-
const out = [];
|
|
744
|
-
const n = path.length;
|
|
745
|
-
let start = 1;
|
|
746
|
-
for (let i = 1; i <= n; i++) {
|
|
747
|
-
if (i !== n && path.charCodeAt(i) !== 47) continue;
|
|
748
|
-
if (i > start) {
|
|
749
|
-
let seg = path.slice(start, i);
|
|
750
|
-
if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
751
|
-
const c0 = seg.charCodeAt(0);
|
|
752
|
-
let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
|
|
753
|
-
if (numeric) {
|
|
754
|
-
for (let k = 1; k < seg.length; k++) {
|
|
755
|
-
const c = seg.charCodeAt(k);
|
|
756
|
-
if (c < 48 || c > 57) { numeric = false; break; }
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
out.push({ key: numeric ? Number(seg) : seg });
|
|
760
|
-
}
|
|
761
|
-
start = i + 1;
|
|
762
|
-
}
|
|
763
|
-
return out;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function createPaddedBuffer(jsonStr) {
|
|
767
|
-
if (typeof Buffer === 'undefined') throw new Error('createPaddedBuffer requires Node.js Buffer');
|
|
768
|
-
const jsonBuf = Buffer.from(jsonStr);
|
|
769
|
-
const padded = Buffer.allocUnsafe(jsonBuf.length + SIMDJSON_PADDING);
|
|
770
|
-
jsonBuf.copy(padded);
|
|
771
|
-
padded.fill(0, jsonBuf.length);
|
|
772
|
-
return { buffer: padded, length: jsonBuf.length };
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Deep-clone a value, copying own symbol keys by reference at every level.
|
|
776
|
-
// Arrays and plain objects are cloned recursively; primitives, RegExp,
|
|
777
|
-
// functions, and other non-plain values are returned as-is. Symbol values
|
|
778
|
-
// (e.g. refinement lists, OPTIONAL markers) are owned by the caller's builder
|
|
779
|
-
// and sharing them is correct — they are never mutated by normalization.
|
|
780
|
-
function _deepCloneWithSymbols(v) {
|
|
781
|
-
if (v === null || typeof v !== 'object') return v;
|
|
782
|
-
if (Array.isArray(v)) {
|
|
783
|
-
const a = new Array(v.length);
|
|
784
|
-
for (let i = 0; i < v.length; i++) a[i] = _deepCloneWithSymbols(v[i]);
|
|
785
|
-
return a;
|
|
786
|
-
}
|
|
787
|
-
// Only clone plain objects (skip RegExp, Date, etc.).
|
|
788
|
-
if (Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) return v;
|
|
789
|
-
const out = Object.create(null);
|
|
790
|
-
for (const k of Object.keys(v)) Object.defineProperty(out, k, { value: _deepCloneWithSymbols(v[k]), writable: true, enumerable: true, configurable: true });
|
|
791
|
-
for (const sym of Object.getOwnPropertySymbols(v)) out[sym] = v[sym];
|
|
792
|
-
return Object.setPrototypeOf(out, Object.prototype);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Normalize a caller-provided schema without mutating the original.
|
|
796
|
-
// Clones only when normalization would change the object (draft-07 keys
|
|
797
|
-
// present or nullable fields present). Internal-only — not exported.
|
|
798
|
-
function _normalizeCallerSchema(s, inheritDraft7) {
|
|
799
|
-
const declares = s && typeof s === 'object' && s.$schema !== undefined
|
|
800
|
-
const needsDraft7 = declares
|
|
801
|
-
? (s.$schema === 'http://json-schema.org/draft-07/schema#' || s.$schema === 'http://json-schema.org/draft-07/schema')
|
|
802
|
-
: !!inheritDraft7
|
|
803
|
-
// One walk answers whether there is anything to do. Almost always there is
|
|
804
|
-
// not, and then the serialize, clone, normalize, serialize, compare below is
|
|
805
|
-
// work spent to find that out. The walk over-reports rather than under, so a
|
|
806
|
-
// schema it clears is one no normalizer would have touched;
|
|
807
|
-
// `tests/test_schema_scan.js` holds that direction against the whole suite.
|
|
808
|
-
if (!needsNormalization(s, needsDraft7)) return s
|
|
809
|
-
|
|
810
|
-
const str = JSON.stringify(s)
|
|
811
|
-
const copy = _deepCloneWithSymbols(s)
|
|
812
|
-
if (needsDraft7) normalizeDraft7(copy, true)
|
|
813
|
-
normalizeNullable(copy)
|
|
814
|
-
normalizeExclusiveBounds(copy)
|
|
815
|
-
// Return original when normalization produced no change, copy otherwise.
|
|
816
|
-
// Kept even though the walk has already said there is work, so that a walk
|
|
817
|
-
// which over-reports still returns exactly what it returned before.
|
|
818
|
-
// Change-detection uses JSON content only; symbols do not affect it.
|
|
819
|
-
return JSON.stringify(copy) === str ? s : copy
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// The identity a document is registered under. Draft-07 ignores every
|
|
823
|
-
// keyword sitting next to `$ref`, so normalization drops them, `$id` among
|
|
824
|
-
// them: that is the right reading for evaluation, where the reference
|
|
825
|
-
// resolves against the retrieval URI rather than the declared `$id`. It is
|
|
826
|
-
// the wrong reading for registration, since `$id` is how the caller names
|
|
827
|
-
// the document. So the identity is read from the normalized copy first and
|
|
828
|
-
// from what the caller passed second. A bare-fragment `$id` is a draft-07
|
|
829
|
-
// anchor rather than a document identity, and normalization has already
|
|
830
|
-
// turned it into `$anchor`, so it is not used here.
|
|
831
|
-
function declaredId(original, normalized) {
|
|
832
|
-
const n = normalized && typeof normalized === 'object' ? normalized.$id : undefined
|
|
833
|
-
if (typeof n === 'string' && n !== '') return n
|
|
834
|
-
const o = original && typeof original === 'object' ? original.$id : undefined
|
|
835
|
-
if (typeof o === 'string' && o !== '' && o[0] !== '#') return o
|
|
836
|
-
return undefined
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// `inheritDraft7` is true when the root schema is draft-07: a retrieved
|
|
840
|
-
// document that declares no dialect is read under the root's draft.
|
|
841
|
-
// The map is derived entirely from what the caller passed, so the same
|
|
842
|
-
// `schemas` gives the same map. A server building one validator per route over
|
|
843
|
-
// a shared registry rebuilt it once per route, normalizing and re-reading the
|
|
844
|
-
// `$id` of every registered schema each time. Keyed by the registry object,
|
|
845
|
-
// and by the draft it is read under, since that changes what normalization
|
|
846
|
-
// does to a document which declares no dialect of its own.
|
|
847
|
-
//
|
|
848
|
-
// Validators share the returned map, so anything that mutates one calls
|
|
849
|
-
// `_ownSchemaMap()` first. There are two such places: registering the vendored
|
|
850
|
-
// meta-schemas during compilation, and `addSchema()`.
|
|
851
|
-
const _schemaMapCache = new WeakMap()
|
|
852
|
-
|
|
853
|
-
function buildSchemaMap(schemas, inheritDraft7) {
|
|
854
|
-
if (!schemas) return null
|
|
855
|
-
const byDraft = _schemaMapCache.get(schemas)
|
|
856
|
-
if (byDraft) {
|
|
857
|
-
const hit = byDraft[inheritDraft7 ? 1 : 0]
|
|
858
|
-
if (hit) return hit
|
|
859
|
-
}
|
|
860
|
-
const map = _buildSchemaMap(schemas, inheritDraft7)
|
|
861
|
-
const slot = byDraft || [null, null]
|
|
862
|
-
slot[inheritDraft7 ? 1 : 0] = map
|
|
863
|
-
if (!byDraft) _schemaMapCache.set(schemas, slot)
|
|
864
|
-
return map
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function _buildSchemaMap(schemas, inheritDraft7) {
|
|
868
|
-
const map = new Map()
|
|
869
|
-
if (Array.isArray(schemas)) {
|
|
870
|
-
for (const s of schemas) {
|
|
871
|
-
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
872
|
-
const id = declaredId(s, normalized)
|
|
873
|
-
if (!id) throw new Error('Schema in schemas option must have $id')
|
|
874
|
-
map.set(id, normalized)
|
|
875
|
-
}
|
|
876
|
-
} else {
|
|
877
|
-
for (const [key, s] of Object.entries(schemas)) {
|
|
878
|
-
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
879
|
-
// A retrieved document is addressable both by the URI it was registered
|
|
880
|
-
// under and by the $id it declares. Registering only the $id makes
|
|
881
|
-
// references to the retrieval URI unresolvable.
|
|
882
|
-
map.set(key, normalized)
|
|
883
|
-
const id = declaredId(s, normalized)
|
|
884
|
-
if (id && id !== key) map.set(id, normalized)
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
return map
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// A schema which names a custom meta-schema in `$schema` is written against
|
|
891
|
-
// whatever dialect that meta-schema declares. A keyword from a vocabulary the
|
|
892
|
-
// dialect does not have is not part of the dialect, so it is an unknown
|
|
893
|
-
// keyword and does not apply. Removing it here means every engine sees the
|
|
894
|
-
// same schema and none of them needs to know about vocabularies.
|
|
895
|
-
//
|
|
896
|
-
// Only the root is consulted. A subschema naming its own `$schema` is its own
|
|
897
|
-
// resource under its own dialect, and the walk stops there rather than
|
|
898
|
-
// applying this dialect's answer to it.
|
|
899
|
-
function _applyVocabularies(schemaObj, original, schemaMap) {
|
|
900
|
-
if (!schemaObj || typeof schemaObj !== 'object') return schemaObj
|
|
901
|
-
const declared = schemaObj.$schema
|
|
902
|
-
if (typeof declared !== 'string') return schemaObj
|
|
903
|
-
const enabled = enabledKeywords(schemaMap.get(declared))
|
|
904
|
-
if (!enabled) return schemaObj
|
|
905
|
-
// `original` is the caller's own object when it reached here unchanged, and
|
|
906
|
-
// that one is never mutated.
|
|
907
|
-
const copy = schemaObj === original
|
|
908
|
-
? _deepCloneWithSymbols(schemaObj)
|
|
909
|
-
: schemaObj
|
|
910
|
-
return stripDisabledKeywords(copy, enabled)
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Compile-cache key for a root schema plus its external schemas. Must include
|
|
914
|
-
// the external schema CONTENT, not just their $ids: two validators can share a
|
|
915
|
-
// root schema string and the same $id while pointing that $id at different
|
|
916
|
-
// schemas (separate app instances, test suites, multi-tenant). Keying on $id
|
|
917
|
-
// alone reuses the wrong compiled validator and silently mis-validates.
|
|
918
|
-
function compileCacheKey(schemaStr, schemaMap) {
|
|
919
|
-
if (!schemaMap || schemaMap.size === 0) return schemaStr
|
|
920
|
-
const parts = []
|
|
921
|
-
for (const [id, s] of schemaMap) parts.push(id + '=' + JSON.stringify(s))
|
|
922
|
-
parts.sort()
|
|
923
|
-
return schemaStr + '\0' + parts.join('\0')
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// Resolve a relative URI ref against a base URI
|
|
927
|
-
function resolveRelativeRef(ref, baseId) {
|
|
928
|
-
if (!baseId || ref.includes('://') || ref.startsWith('#')) return ref
|
|
929
|
-
const lastSlash = baseId.lastIndexOf('/')
|
|
930
|
-
if (lastSlash < 0) return ref
|
|
931
|
-
return baseId.substring(0, lastSlash + 1) + ref
|
|
932
|
-
}
|
|
690
|
+
// The parts no Validator calls: TypeScript generation, the error renderers,
|
|
691
|
+
// the spec output format, the retry message for language models, the schema
|
|
692
|
+
// description and the suggestion helper. ata-validator/lite leaves them out.
|
|
693
|
+
// Each loads its module on first call. None of them is on the path to a first
|
|
694
|
+
// validated request, and together they were about a sixth of what requiring
|
|
695
|
+
// the package cost.
|
|
696
|
+
function toTypeScript (...a) { return require('./lib/ts-gen').toTypeScript(...a); }
|
|
697
|
+
function renderPretty (...a) { return require('./lib/render-pretty').renderPretty(...a); }
|
|
698
|
+
function renderCompact (...a) { return require('./lib/render-compact').renderCompact(...a); }
|
|
699
|
+
function toOutput (...a) { return require('./lib/output-format').toOutput(...a); }
|
|
700
|
+
function toRetryMessage (...a) { return require('./lib/retry-message').toRetryMessage(...a); }
|
|
701
|
+
function describeSchema (...a) { return require('./lib/describe-schema').describeSchema(...a); }
|
|
702
|
+
function renderJSON (...a) { return require('./lib/render-json').renderJSON(...a); }
|
|
933
703
|
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const baseId = hashIdx >= 0 ? ref.slice(0, hashIdx) : ref
|
|
943
|
-
const fragment = hashIdx >= 0 ? ref.slice(hashIdx + 1) : ''
|
|
944
|
-
if (!baseId) return null
|
|
945
|
-
let base = null
|
|
946
|
-
if (schemaMap.has(baseId)) base = schemaMap.get(baseId)
|
|
947
|
-
else if (!ref.includes('://')) {
|
|
948
|
-
for (const [id, s] of schemaMap) {
|
|
949
|
-
if (id.endsWith('/' + baseId)) { base = s; break }
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
if (!base) return null
|
|
953
|
-
if (!fragment) return base
|
|
954
|
-
let target = base
|
|
955
|
-
for (const part of fragment.split('/')) {
|
|
956
|
-
if (part === '') continue
|
|
957
|
-
if (target == null || typeof target !== 'object') return null
|
|
958
|
-
target = target[part.replace(/~1/g, '/').replace(/~0/g, '~')]
|
|
959
|
-
}
|
|
960
|
-
return target == null ? null : target
|
|
704
|
+
// Walk a JSON pointer (RFC 6901 escapes) into a data tree. Mirrors the helper
|
|
705
|
+
// inside lib/suggestions.js, kept local to avoid exporting an internal.
|
|
706
|
+
function _walkPointer (root, pointer) {
|
|
707
|
+
if (!pointer) return root;
|
|
708
|
+
const parts = pointer.replace(/^\//, '').split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
709
|
+
let cur = root;
|
|
710
|
+
for (const p of parts) { if (cur == null) return undefined; cur = cur[p]; }
|
|
711
|
+
return cur;
|
|
961
712
|
}
|
|
962
713
|
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
978
|
-
if (!s.properties) return s
|
|
979
|
-
// Property-level refs: substitute the resolved target so coercion sees `type`.
|
|
980
|
-
let cloned = null
|
|
981
|
-
for (const key of Object.keys(s.properties)) {
|
|
982
|
-
const p = s.properties[key]
|
|
983
|
-
if (p && typeof p === 'object' && p.$ref && !p.type) {
|
|
984
|
-
const t = resolveRefForPreprocess(p.$ref, schemaMap)
|
|
985
|
-
if (t && typeof t === 'object') {
|
|
986
|
-
if (!cloned) { cloned = Object.assign({}, s); cloned.properties = Object.assign({}, s.properties) }
|
|
987
|
-
cloned.properties[key] = t
|
|
988
|
-
}
|
|
714
|
+
// Post-hoc suggestion enrichment for AOT-compiled validators. The standalone
|
|
715
|
+
// modules do not embed the suggestion engine, so consumers who want
|
|
716
|
+
// suggestions pass the error array through this after validation.
|
|
717
|
+
function attachSuggestions (errors, data) {
|
|
718
|
+
if (!errors) return errors;
|
|
719
|
+
const { suggestFor } = require('./lib/suggestions');
|
|
720
|
+
const { reprValue } = require('./lib/enrich-error');
|
|
721
|
+
for (const e of errors) {
|
|
722
|
+
if (!e || e.suggestion) continue;
|
|
723
|
+
let received = e.received;
|
|
724
|
+
if (received === undefined && data !== undefined) {
|
|
725
|
+
const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
|
|
726
|
+
const raw = _walkPointer(data, ptr);
|
|
727
|
+
if (raw !== undefined || ptr === '') received = reprValue(raw);
|
|
989
728
|
}
|
|
729
|
+
const probe = received !== undefined && e.received === undefined
|
|
730
|
+
? Object.assign({}, e, { received })
|
|
731
|
+
: e;
|
|
732
|
+
const s = suggestFor(probe, data);
|
|
733
|
+
if (s) e.suggestion = s;
|
|
990
734
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
// the caller's schema normalized on a clone (the caller's object is never
|
|
996
|
-
// touched), `format` stripped under `assertFormat: false`, and the custom
|
|
997
|
-
// keyword scan. The accessors then step aside for own data properties, so
|
|
998
|
-
// every later read is a plain field.
|
|
999
|
-
function _materializeSchema(self) {
|
|
1000
|
-
const raw = self._rawSchema;
|
|
1001
|
-
const options = self._options;
|
|
1002
|
-
let schemaObj = _normalizeCallerSchema(raw);
|
|
1003
|
-
const isCallers = self._rawIsCallers && schemaObj === raw;
|
|
1004
|
-
if (options.assertFormat === false) {
|
|
1005
|
-
schemaObj = stripFormatAssertions(isCallers ? _deepCloneWithSymbols(schemaObj) : schemaObj);
|
|
1006
|
-
}
|
|
1007
|
-
const usesKeywords = self._keywords !== null && schemaUsesKeywords(schemaObj, self._keywords);
|
|
1008
|
-
Object.defineProperty(self, '_schemaObj', { value: schemaObj, writable: true, configurable: true, enumerable: true });
|
|
1009
|
-
Object.defineProperty(self, '_usesKeywords', { value: usesKeywords, writable: true, configurable: true, enumerable: true });
|
|
1010
|
-
Object.defineProperty(self, '_schemaIsCallers', { value: isCallers && schemaObj === raw, writable: true, configurable: true, enumerable: true });
|
|
1011
|
-
return schemaObj;
|
|
735
|
+
// AOT modules import nothing, so this is their only route to a frame. The
|
|
736
|
+
// caller holds the original object and ran no preprocessing through here.
|
|
737
|
+
require('./lib/diagnostic-source').setDiagnosticSource(errors, { data, mutatesInput: false });
|
|
738
|
+
return errors;
|
|
1012
739
|
}
|
|
1013
740
|
|
|
1014
|
-
class Validator {
|
|
1015
|
-
constructor(schema, opts) {
|
|
1016
|
-
const options = opts || {};
|
|
1017
|
-
|
|
1018
|
-
// Ultra-fast path: same schema object reference -> return cached instance
|
|
1019
|
-
// JS constructor returning an object makes `new` return that object
|
|
1020
|
-
// Cost: one WeakMap lookup. No property copy, no setup, nothing.
|
|
1021
|
-
if (!opts && typeof schema === "object" && schema !== null) {
|
|
1022
|
-
const hit = _identityCache.get(schema);
|
|
1023
|
-
if (hit) return hit;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// The schema is not walked here. Normalization (draft-07 rewrites,
|
|
1027
|
-
// nullable, `assertFormat: false`) and the scan that decides whether any
|
|
1028
|
-
// of it is needed run on the first read of `_schemaObj`, which is the
|
|
1029
|
-
// first compile. Construction is the object and its fields; a server
|
|
1030
|
-
// building a validator per request pays nothing for a schema it never
|
|
1031
|
-
// uses, and a benchmark timing construction measures construction.
|
|
1032
|
-
// When schema is a string, JSON.parse already produces a fresh object.
|
|
1033
|
-
const raw = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
1034
|
-
const rootIsDraft7 = !!(raw && typeof raw === 'object' && typeof raw.$schema === 'string' &&
|
|
1035
|
-
(raw.$schema === 'http://json-schema.org/draft-07/schema#' || raw.$schema === 'http://json-schema.org/draft-07/schema'));
|
|
1036
|
-
|
|
1037
|
-
// Built here rather than below because `$vocabulary` is resolved against
|
|
1038
|
-
// it, and that resolution waits until compilation so a meta-schema
|
|
1039
|
-
// registered by addSchema() still counts.
|
|
1040
|
-
const shared = buildSchemaMap(options.schemas, rootIsDraft7);
|
|
1041
|
-
const schemaMap = shared || new Map();
|
|
1042
|
-
this._schemaMapShared = shared !== null;
|
|
1043
|
-
this._vocabulariesApplied = false;
|
|
1044
|
-
|
|
1045
|
-
// Custom keywords, normalized once. `_usesKeywords` (resolved with the
|
|
1046
|
-
// schema) is what routes the schema to the interpreted engine and keeps
|
|
1047
|
-
// it out of the shared compile cache; a schema that registers keywords
|
|
1048
|
-
// but uses none of them takes the ordinary path.
|
|
1049
|
-
this._keywords = normalizeKeywords(options.keywords);
|
|
1050
|
-
|
|
1051
|
-
this._schemaStr = null; // lazy: computed on first use
|
|
1052
|
-
this._rawSchema = raw;
|
|
1053
|
-
this._rawIsCallers = typeof schema !== "string";
|
|
1054
|
-
this._options = options;
|
|
1055
|
-
this._noOpts = !opts;
|
|
1056
|
-
// engine: 'interpreter' keeps this validator off code generation: no
|
|
1057
|
-
// `new Function`, no shared compile cache, the eval-free interpreted
|
|
1058
|
-
// engine answers validate(), isValidObject() and validateJSON(). For a
|
|
1059
|
-
// schema that arrives from outside the trust boundary (a plugin's
|
|
1060
|
-
// declared config shape, a tenant's upload), where turning it into source
|
|
1061
|
-
// is not an acceptable execution model. ATA_FORCE_NAPI does this for the
|
|
1062
|
-
// whole process; the option does it for one validator. A misspelling
|
|
1063
|
-
// must not fall through to codegen, so anything else is refused.
|
|
1064
|
-
if (options.engine !== undefined && options.engine !== 'auto' && options.engine !== 'interpreter') {
|
|
1065
|
-
throw new TypeError("engine must be 'auto' or 'interpreter', got " + JSON.stringify(options.engine));
|
|
1066
|
-
}
|
|
1067
|
-
this._interpretOnly = options.engine === 'interpreter';
|
|
1068
|
-
this._initialized = false;
|
|
1069
|
-
this._nativeReady = false;
|
|
1070
|
-
this._compiled = null;
|
|
1071
|
-
this._fastSlot = -1;
|
|
1072
|
-
this._jsFn = null;
|
|
1073
|
-
this._engine = undefined;
|
|
1074
|
-
this._preprocess = null;
|
|
1075
|
-
this._applyDefaults = null;
|
|
1076
|
-
|
|
1077
|
-
// Schema map for cross-schema $ref resolution
|
|
1078
|
-
this._schemaMap = schemaMap;
|
|
1079
|
-
|
|
1080
|
-
// User-supplied format checkers: { formatName: (value) => boolean }.
|
|
1081
|
-
// Looked up at runtime when a schema references a format the built-in
|
|
1082
|
-
// registry does not know about.
|
|
1083
|
-
this._userFormats = options.formats || null;
|
|
1084
|
-
|
|
1085
|
-
// Verbose mode: when on, errors carry parentSchema (the schema object that
|
|
1086
|
-
// produced the error). Matches ajv's `verbose: true` behavior.
|
|
1087
|
-
this._verbose = !!options.verbose;
|
|
1088
|
-
|
|
1089
|
-
// strictSchema: authoring-time checks, off by default. A mistyped keyword
|
|
1090
|
-
// is the one schema mistake that fails open: to every dialect `maxLenght`
|
|
1091
|
-
// is an annotation, so the constraint the author meant is simply absent
|
|
1092
|
-
// and previously invalid data validates. `true` throws here, at
|
|
1093
|
-
// construction, with every finding; `'log'` reports through
|
|
1094
|
-
// `options.logger` or the console and continues. The check runs on the
|
|
1095
|
-
// schema as written, before any normalization touches it.
|
|
1096
|
-
if (options.strictSchema === true || options.strictSchema === 'log') {
|
|
1097
|
-
const { checkSchemaStrict } = require('./lib/strict-check');
|
|
1098
|
-
const problems = checkSchemaStrict(schema, { userKeywords: options.keywords || null });
|
|
1099
|
-
if (problems.length > 0) {
|
|
1100
|
-
const text = problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n');
|
|
1101
|
-
if (options.strictSchema === true) {
|
|
1102
|
-
throw new Error(text);
|
|
1103
|
-
}
|
|
1104
|
-
const logger = options.logger;
|
|
1105
|
-
if (logger !== false) {
|
|
1106
|
-
const warn = logger && typeof logger.warn === 'function' ? logger.warn.bind(logger) : console.warn;
|
|
1107
|
-
warn(text);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
// richErrors: default true. Only the literal `false` opts back into the
|
|
1113
|
-
// v0.14 error shape (no code/expected/received/docUrl, no aliases).
|
|
1114
|
-
this._richErrors = options && options.richErrors === false ? false : true;
|
|
1115
|
-
|
|
1116
|
-
// Optional schema source descriptor. When supplied, the renderer pipeline
|
|
1117
|
-
// can attach a `schemaSource` frame to enriched errors.
|
|
1118
|
-
this._source = options && options.source && typeof options.source === 'object'
|
|
1119
|
-
? { path: String(options.source.path || ''), content: String(options.source.content || '') }
|
|
1120
|
-
: null;
|
|
1121
|
-
|
|
1122
|
-
// Build a JSON pointer -> position map for the schema text once at
|
|
1123
|
-
// construction so each runtime error can resolve `schemaSource` without
|
|
1124
|
-
// re-scanning the source on every validate() call.
|
|
1125
|
-
if (this._source) {
|
|
1126
|
-
const { buildPositionMap } = require('./lib/source-positions');
|
|
1127
|
-
this._schemaPositions = buildPositionMap(this._source.content);
|
|
1128
|
-
} else {
|
|
1129
|
-
this._schemaPositions = null;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Per-validate data position cache. Populated by validateJSON before
|
|
1133
|
-
// dispatching to inner validate(); consulted by the rich-error wrap
|
|
1134
|
-
// to attach dataFrame entries to each enriched error.
|
|
1135
|
-
this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
|
|
1136
|
-
this._lastRawInput = null;
|
|
1137
|
-
// undefined: not built yet. null: this schema has no scanner.
|
|
1138
|
-
this._scanner = undefined;
|
|
1139
|
-
// Checks a wrapper registered through _extendChecks. Declared here so
|
|
1140
|
-
// registering one does not change the instance's shape.
|
|
1141
|
-
this._verdictTail = null;
|
|
1142
|
-
this._validateTail = null;
|
|
1143
|
-
this._entryExt = null;
|
|
1144
|
-
|
|
1145
|
-
// Public methods start as memoized accessors on the prototype; nothing is
|
|
1146
|
-
// allocated per instance until one is first read. See _defineLazyMethod
|
|
1147
|
-
// below the class.
|
|
1148
|
-
|
|
1149
|
-
// "~standard" (Standard Schema V1) is a lazy prototype accessor too;
|
|
1150
|
-
// see below the class. Consumers only pay for it if they read it.
|
|
1151
|
-
|
|
1152
|
-
// The identity cache, which lets a later `new Validator(sameSchema)` return
|
|
1153
|
-
// this instance, is filled on the first compile rather than here. A WeakMap
|
|
1154
|
-
// entry is an ephemeron the collector has to trace separately, and setting
|
|
1155
|
-
// one cost about 780 ns against 150 for the rest of this constructor, five
|
|
1156
|
-
// times over for an instance that may never validate anything. Once an
|
|
1157
|
-
// instance has compiled, the shortcut behaves as before.
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// `$vocabulary` says which keywords the dialect has, and answering needs the
|
|
1161
|
-
// meta-schema, which addSchema() may only have registered just now. Run once,
|
|
1162
|
-
// before anything reads the schema, and before `_schemaStr` is computed from
|
|
1163
|
-
// it. After this addSchema() is refused, so the answer cannot go stale.
|
|
1164
|
-
// Whether validation is preceded by a pass that rewrites the input:
|
|
1165
|
-
// coercion, removal of undeclared keys, or filling in defaults. The verdict
|
|
1166
|
-
// methods have to take the same path when it is, so the quick bindings that
|
|
1167
|
-
// answer from the compiled function alone are not used for these validators.
|
|
1168
|
-
_needsPreprocess() {
|
|
1169
|
-
const o = this._options;
|
|
1170
|
-
if (o.coerceTypes || o.removeAdditional) return true;
|
|
1171
|
-
if (o.useDefaults === false) return false;
|
|
1172
|
-
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
1173
|
-
return this._schemaStr.includes('"default"');
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
_pos() {
|
|
1177
|
-
return this._posCache || (this._posCache = _createPosCache());
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
_ensureVocabularies() {
|
|
1181
|
-
if (this._vocabulariesApplied) return;
|
|
1182
|
-
this._vocabulariesApplied = true;
|
|
1183
|
-
const stripped = _applyVocabularies(
|
|
1184
|
-
this._schemaObj,
|
|
1185
|
-
this._schemaIsCallers ? this._schemaObj : null,
|
|
1186
|
-
this._schemaMap,
|
|
1187
|
-
);
|
|
1188
|
-
if (stripped !== this._schemaObj) {
|
|
1189
|
-
this._schemaObj = stripped;
|
|
1190
|
-
this._schemaStr = null;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
_ensureCompiled() {
|
|
1195
|
-
if (this._initialized) return;
|
|
1196
|
-
this._ensureVocabularies();
|
|
1197
|
-
this._initialized = true;
|
|
1198
|
-
|
|
1199
|
-
const schemaObj = this._schemaObj;
|
|
1200
|
-
const options = this._options;
|
|
1201
|
-
|
|
1202
|
-
// Lazy stringify — only computed here, not in constructor
|
|
1203
|
-
if (!this._schemaStr) this._schemaStr = JSON.stringify(schemaObj);
|
|
1204
|
-
|
|
1205
|
-
// A $ref to a meta-schema resolves from the vendored copies, so
|
|
1206
|
-
// "validate this schema against its dialect" needs no network and no
|
|
1207
|
-
// caller-supplied registry. Only schemas that mention json-schema.org in a
|
|
1208
|
-
// reference pay for the lookup.
|
|
1209
|
-
if (this._schemaStr.includes('json-schema.org/draft')) {
|
|
1210
|
-
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
1211
|
-
this._ownSchemaMap();
|
|
1212
|
-
for (const [id, meta] of METASCHEMAS) {
|
|
1213
|
-
const bare = id.replace(/#$/, '');
|
|
1214
|
-
for (const key of [id, bare, bare + '#', bare.replace(/^https:/, 'http:'), bare.replace(/^http:/, 'https:')]) {
|
|
1215
|
-
if (!this._schemaMap.has(key)) this._schemaMap.set(key, meta);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// Check cache first -- reuse compiled functions for same schema
|
|
1221
|
-
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
1222
|
-
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
1223
|
-
var _forceNapi = this._interpretOnly || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI);
|
|
1224
|
-
// Custom formats are JS functions: bypass the compile cache since they can
|
|
1225
|
-
// differ between validators that share the same schema string. An
|
|
1226
|
-
// interpreter-only validator never touches it either way: a function a
|
|
1227
|
-
// trusted validator compiled for the same schema string must not answer
|
|
1228
|
-
// for it.
|
|
1229
|
-
const cached = (this._userFormats || this._usesKeywords || _forceNapi) ? null : _compileCache.get(mapKey);
|
|
1230
|
-
let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
|
|
1231
|
-
// v1 removes the bookending requirement for $dynamicRef. Only the
|
|
1232
|
-
// interpreted engine implements that; the JS compiler and the native
|
|
1233
|
-
// engine both resolve the 2020-12 way, so a v1 schema using the keyword
|
|
1234
|
-
// goes to the interpreter rather than being validated under the wrong
|
|
1235
|
-
// dialect. Schemas without $dynamicRef are unaffected: v1 and 2020-12
|
|
1236
|
-
// agree on everything else ata implements.
|
|
1237
|
-
this._v1Dynamic =
|
|
1238
|
-
isV1Dialect(schemaObj) &&
|
|
1239
|
-
(this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"'));
|
|
1240
|
-
//
|
|
1241
|
-
// Where source cannot be turned into a function, neither JS path is usable
|
|
1242
|
-
// either. The closure path does not call `new Function` itself, so it
|
|
1243
|
-
// survives the block and would quietly handle schemas it gets wrong; the
|
|
1244
|
-
// interpreted engine is both eval-free and more correct, so go straight
|
|
1245
|
-
// there. The forced case is decided before the probe: the probe is a
|
|
1246
|
-
// `new Function` too, and a validator that promised none must not run it.
|
|
1247
|
-
if (_forceNapi || this._v1Dynamic || this._usesKeywords || !codegenAvailable()) {
|
|
1248
|
-
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
1249
|
-
// `full` separates an entry that holds every compiled function from one
|
|
1250
|
-
// the verdict-only fast path seeded, where `combined` and `errFn` are null
|
|
1251
|
-
// because nothing has tried to build them yet. Both halves of that
|
|
1252
|
-
// distinction are null, and reading the second as the first costs this
|
|
1253
|
-
// schema its generated error function for the life of the process.
|
|
1254
|
-
} else if (cached && cached.jsFn !== undefined) {
|
|
1255
|
-
// `full` says the error and combined functions exist too. An entry
|
|
1256
|
-
// without it still carries a verdict function worth reusing; the pair is
|
|
1257
|
-
// built by _buildErr/_buildCombined below when something asks, and the
|
|
1258
|
-
// entry is upgraded then. `undefined` in `combined`/`errFn` means not
|
|
1259
|
-
// built yet; `null` means the compiler declined. Those two must never
|
|
1260
|
-
// blur: reading the first as the second is the bug this cache had once
|
|
1261
|
-
// already, and it silently cost schemas their generated error function.
|
|
1262
|
-
jsFn = cached.jsFn;
|
|
1263
|
-
jsCombinedFn = cached.combined;
|
|
1264
|
-
jsErrFn = cached.errFn;
|
|
1265
|
-
_isCodegen = !!cached.isCodegen;
|
|
1266
|
-
this._engine = _isCodegen ? 'codegen' : jsFn ? 'closure' : null;
|
|
1267
|
-
} else {
|
|
1268
|
-
const uf = this._userFormats;
|
|
1269
|
-
const _cgFn = compileToJSCodegen(schemaObj, sm, uf);
|
|
1270
|
-
jsFn = _cgFn || compileToJS(schemaObj, null, sm);
|
|
1271
|
-
// Only the verdict is compiled here. The error and combined generators
|
|
1272
|
-
// are the other two thirds of a cold first call (8.2, 10.4 and 7.8 ms on
|
|
1273
|
-
// a 120-property config schema, most of it V8 compiling each generator
|
|
1274
|
-
// the first time it is entered), and a caller that never reads an error
|
|
1275
|
-
// never needs them. _buildErr/_buildCombined compile them on demand.
|
|
1276
|
-
jsCombinedFn = undefined;
|
|
1277
|
-
jsErrFn = undefined;
|
|
1278
|
-
_isCodegen = !!_cgFn;
|
|
1279
|
-
this._engine = _cgFn ? 'codegen' : jsFn ? 'closure' : null;
|
|
1280
|
-
if (!uf) {
|
|
1281
|
-
_compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
this._jsFn = jsFn;
|
|
1285
|
-
if (this._engine === undefined) this._engine = null;
|
|
1286
|
-
|
|
1287
|
-
// Data mutators -- try codegen first (12x faster), fallback to closure arrays.
|
|
1288
|
-
// Follow cross-refs so coercion/defaults/removeAdditional see the referenced
|
|
1289
|
-
// shape (e.g. Fastify `params: { $ref: 'shared#' }` or property refs like
|
|
1290
|
-
// `{ id: { $ref: 'shared#/properties/id' } }`).
|
|
1291
|
-
const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
|
|
1292
|
-
// The mutator pass (defaults, coercion, removal) is generated source too,
|
|
1293
|
-
// with the schema's `default` values embedded; an interpreter-only
|
|
1294
|
-
// validator takes the closure mutators instead.
|
|
1295
|
-
let preprocess = this._interpretOnly ? null : buildPreprocessCodegen(preprocessSchema, options);
|
|
1296
|
-
if (!preprocess) {
|
|
1297
|
-
const applyDefaults = options.useDefaults === false ? null : buildDefaultsApplier(preprocessSchema);
|
|
1298
|
-
const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
|
|
1299
|
-
const applyRemove = options.removeAdditional
|
|
1300
|
-
? buildRemover(preprocessSchema)
|
|
1301
|
-
: null;
|
|
1302
|
-
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
1303
|
-
preprocess =
|
|
1304
|
-
mutators.length === 0
|
|
1305
|
-
? null
|
|
1306
|
-
: mutators.length === 1
|
|
1307
|
-
? mutators[0]
|
|
1308
|
-
: (data) => {
|
|
1309
|
-
for (let i = 0; i < mutators.length; i++) mutators[i](data);
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
this._applyDefaults = preprocess;
|
|
1313
|
-
// Whether validate() can change the caller's object before the verdict.
|
|
1314
|
-
// This is a capability, not an option: `useDefaults` is on by default, but
|
|
1315
|
-
// buildDefaultsApplier returns null when the schema declares no defaults,
|
|
1316
|
-
// so a plain schema is genuinely non-mutating. The renderers refuse to
|
|
1317
|
-
// synthesize a frame when this is true, because a frame built from mutated
|
|
1318
|
-
// data would show the reader a value they never sent.
|
|
1319
|
-
this._mutatesInput = !!(preprocess || options.coerceTypes || options.removeAdditional);
|
|
1320
|
-
this._preprocess = preprocess;
|
|
1321
|
-
|
|
1322
|
-
// removeAdditional alone, the common parse-and-strip use: a verdict function
|
|
1323
|
-
// that deletes unknown keys in the walk it already makes, where the pass
|
|
1324
|
-
// above walks every object a second time just to find them. It answers
|
|
1325
|
-
// the documents it accepts; anything it rejects takes the full path below,
|
|
1326
|
-
// which removes, validates and reports exactly as before, so a rejected
|
|
1327
|
-
// document is left as clean as it always was. The generator declines any
|
|
1328
|
-
// schema where deleting during the walk could change an answer.
|
|
1329
|
-
let fusedRemove = null;
|
|
1330
|
-
if (preprocess && options.removeAdditional && !options.coerceTypes && !this._interpretOnly &&
|
|
1331
|
-
!this._userFormats && !this._usesKeywords && !this._schemaStr.includes('"default"')) {
|
|
1332
|
-
try {
|
|
1333
|
-
fusedRemove = compileToJSCodegen(schemaObj, this._schemaMap.size > 0 ? this._schemaMap : null, null, { removeAdditional: true });
|
|
1334
|
-
} catch {
|
|
1335
|
-
fusedRemove = null;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// Detect if schema is "selective" -- doesn't recurse into arrays/deep objects.
|
|
1340
|
-
const hasArrayTraversal =
|
|
1341
|
-
schemaObj &&
|
|
1342
|
-
(schemaObj.items ||
|
|
1343
|
-
schemaObj.prefixItems ||
|
|
1344
|
-
schemaObj.contains ||
|
|
1345
|
-
(schemaObj.properties &&
|
|
1346
|
-
Object.values(schemaObj.properties).some(
|
|
1347
|
-
(p) => p && (p.items || p.prefixItems || p.contains),
|
|
1348
|
-
)));
|
|
1349
|
-
const useSimdjsonForLarge = !hasArrayTraversal;
|
|
1350
|
-
|
|
1351
|
-
// Build the generators the compile step left out, each only when something
|
|
1352
|
-
// asks for it, and upgrade the shared cache entry. A first rejection needs
|
|
1353
|
-
// one of the two, not both; building both was a millisecond of V8 compiling
|
|
1354
|
-
// a generator nobody called. `undefined` means not built yet; `null` means
|
|
1355
|
-
// the compiler declined. Conflating those is what once cost every schema
|
|
1356
|
-
// its generated error function for the life of the process, so they stay
|
|
1357
|
-
// apart, and `full` is set only once both exist.
|
|
1358
|
-
const _upgradeCacheEntry = () => {
|
|
1359
|
-
if (this._userFormats) return;
|
|
1360
|
-
const entry = _compileCache.get(mapKey);
|
|
1361
|
-
if (entry && entry.jsFn === jsFn) {
|
|
1362
|
-
if (jsCombinedFn !== undefined) entry.combined = jsCombinedFn;
|
|
1363
|
-
if (jsErrFn !== undefined) entry.errFn = jsErrFn;
|
|
1364
|
-
if (jsCombinedFn !== undefined && jsErrFn !== undefined) entry.full = true;
|
|
1365
|
-
}
|
|
1366
|
-
};
|
|
1367
|
-
const _buildCombined = () => {
|
|
1368
|
-
if (jsCombinedFn !== undefined) return;
|
|
1369
|
-
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, this._userFormats) || null;
|
|
1370
|
-
_upgradeCacheEntry();
|
|
1371
|
-
};
|
|
1372
|
-
const _buildErr = () => {
|
|
1373
|
-
if (jsErrFn !== undefined) return;
|
|
1374
|
-
jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, this._userFormats) || null;
|
|
1375
|
-
_upgradeCacheEntry();
|
|
1376
|
-
};
|
|
1377
|
-
|
|
1378
|
-
if (jsFn) {
|
|
1379
|
-
// errFn: use JS codegen if safe, else native fallback (only when native
|
|
1380
|
-
// is available). Environments without the native addon — Cloudflare
|
|
1381
|
-
// Workers, browser, Bun without N-API — get a JS-only fallback so the
|
|
1382
|
-
// invalid path doesn't dereference a null _compiled.
|
|
1383
|
-
const hasUnevaluated = schemaObj && (schemaObj.unevaluatedProperties !== undefined || schemaObj.unevaluatedItems !== undefined || this._schemaStr.includes('unevaluatedProperties') || this._schemaStr.includes('unevaluatedItems'))
|
|
1384
|
-
const hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
|
|
1385
|
-
// Native-less error path: the interpreted engine re-validates failing
|
|
1386
|
-
// data to produce full errors. If it disagrees with the codegen verdict
|
|
1387
|
-
// (it should not), a generic error keeps the result consistent.
|
|
1388
|
-
let _interp = null;
|
|
1389
|
-
const jsOnlyFallback = (d) => {
|
|
1390
|
-
if (jsFn(d)) return { valid: true, data: d, errors: [] };
|
|
1391
|
-
if (!_interp) {
|
|
1392
|
-
const { createInterpreter } = require('./lib/interpreter');
|
|
1393
|
-
_interp = createInterpreter(schemaObj, {
|
|
1394
|
-
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1395
|
-
formats: this._userFormats,
|
|
1396
|
-
v1: isV1Dialect(schemaObj),
|
|
1397
|
-
keywords: this._keywords,
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
const r = _interp.validate(d);
|
|
1401
|
-
if (!r.valid) return r;
|
|
1402
|
-
return {
|
|
1403
|
-
valid: false,
|
|
1404
|
-
errors: [{
|
|
1405
|
-
keyword: 'validation',
|
|
1406
|
-
instancePath: '',
|
|
1407
|
-
schemaPath: '',
|
|
1408
|
-
params: {},
|
|
1409
|
-
message: 'schema validation failed'
|
|
1410
|
-
}]
|
|
1411
|
-
};
|
|
1412
|
-
};
|
|
1413
|
-
// The error generator declines unevaluated*; the interpreted engine
|
|
1414
|
-
// reports those schemas correctly, so failing data is re-validated
|
|
1415
|
-
// there. This used to be a placeholder error with no keyword and no
|
|
1416
|
-
// path, which hid whatever had actually failed.
|
|
1417
|
-
// Resolved on the first rejection rather than at compile time, because
|
|
1418
|
-
// building the generator behind it is two thirds of what a first call
|
|
1419
|
-
// costs and a caller that never reads an error never needs it. The probe
|
|
1420
|
-
// moves here with it: it calls the generated function, so it cannot run
|
|
1421
|
-
// before the function exists.
|
|
1422
|
-
let _errOnlyImpl = null;
|
|
1423
|
-
const errOnly = (d) => {
|
|
1424
|
-
if (_errOnlyImpl === null) {
|
|
1425
|
-
_buildErr();
|
|
1426
|
-
let safe = null;
|
|
1427
|
-
if (jsErrFn) {
|
|
1428
|
-
try {
|
|
1429
|
-
jsErrFn({}, true);
|
|
1430
|
-
safe = (x) => jsErrFn(x, true);
|
|
1431
|
-
} catch {}
|
|
1432
|
-
}
|
|
1433
|
-
_errOnlyImpl =
|
|
1434
|
-
safe ||
|
|
1435
|
-
(hasUnevaluated || !native
|
|
1436
|
-
? jsOnlyFallback
|
|
1437
|
-
: hasDynRef
|
|
1438
|
-
? (x) => {
|
|
1439
|
-
this._ensureNative();
|
|
1440
|
-
return this._compiled.validateJSON(JSON.stringify(x));
|
|
1441
|
-
}
|
|
1442
|
-
: (x) => {
|
|
1443
|
-
this._ensureNative();
|
|
1444
|
-
return this._compiled.validate(x);
|
|
1445
|
-
});
|
|
1446
|
-
}
|
|
1447
|
-
return _mustReject(_errOnlyImpl(d));
|
|
1448
|
-
};
|
|
1449
|
-
|
|
1450
|
-
// Best path: combined validator (single pass, validates + collects errors)
|
|
1451
|
-
// Valid data: returns VALID_RESULT, no allocation
|
|
1452
|
-
// Invalid data: collects errors in one pass (no double validation)
|
|
1453
|
-
// Fallback: hybridFn or jsFn + errFn for schemas combined can't handle
|
|
1454
|
-
// Test combined at compile time -- some schemas produce broken combined code
|
|
1455
|
-
// Test combined at compile time -- some schemas (e.g. if/then/else)
|
|
1456
|
-
// produce broken combined code that crashes on certain inputs.
|
|
1457
|
-
// We probe with diverse data; if any throws, fall back to hybrid.
|
|
1458
|
-
let _combinedProbed = false;
|
|
1459
|
-
let _safeCombined = null;
|
|
1460
|
-
const combinedIfSafe = () => {
|
|
1461
|
-
if (_combinedProbed) return _safeCombined;
|
|
1462
|
-
_combinedProbed = true;
|
|
1463
|
-
_buildCombined();
|
|
1464
|
-
if (jsCombinedFn) {
|
|
1465
|
-
try {
|
|
1466
|
-
const probe = {};
|
|
1467
|
-
// Populate probe with one key per known property to trigger nested paths
|
|
1468
|
-
if (schemaObj && schemaObj.properties) {
|
|
1469
|
-
for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
|
|
1470
|
-
}
|
|
1471
|
-
if (schemaObj && schemaObj.if && schemaObj.if.properties) {
|
|
1472
|
-
for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
|
|
1473
|
-
}
|
|
1474
|
-
jsCombinedFn(probe);
|
|
1475
|
-
jsCombinedFn({});
|
|
1476
|
-
jsCombinedFn(null);
|
|
1477
|
-
jsCombinedFn(0);
|
|
1478
|
-
_safeCombined = jsCombinedFn;
|
|
1479
|
-
} catch {}
|
|
1480
|
-
}
|
|
1481
|
-
return _safeCombined;
|
|
1482
|
-
};
|
|
1483
|
-
|
|
1484
|
-
// What the hybrid path hands to its error slot: the combined function
|
|
1485
|
-
// when it is usable, since it validates and collects in one pass, and
|
|
1486
|
-
// the error generator otherwise. Same order the eager code chose, just
|
|
1487
|
-
// chosen on the first rejection.
|
|
1488
|
-
let _errPreferredImpl = null;
|
|
1489
|
-
const errPreferCombined = (d) => {
|
|
1490
|
-
if (_errPreferredImpl === null) _errPreferredImpl = combinedIfSafe() || errOnly;
|
|
1491
|
-
return _mustReject(_errPreferredImpl(d));
|
|
1492
|
-
};
|
|
1493
|
-
|
|
1494
|
-
// The boolean engine is the verdict authority for these paths; the
|
|
1495
|
-
// final lazy wrapper uses it to skip error construction entirely.
|
|
1496
|
-
if (!hasDynRef || _isCodegen) this._fastVerdict = preprocess ? null : jsFn;
|
|
1497
|
-
|
|
1498
|
-
if (options.abortEarly && jsFn && !hasDynRef) {
|
|
1499
|
-
// abortEarly: do NOT enrich. Skip position lookups, suggestions, source maps.
|
|
1500
|
-
// This is the perf-critical path for edge gateways. The richErrors wrap
|
|
1501
|
-
// below recognises the ATA9000 stub keyword and passes the frozen result
|
|
1502
|
-
// through unchanged, so a single shared object is returned per failure.
|
|
1503
|
-
const _fn = jsFn;
|
|
1504
|
-
this.validate = preprocess
|
|
1505
|
-
? (data) => { preprocess(data); return _fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
|
|
1506
|
-
: (data) => (_fn(data) ? VALID_RESULT : ABORT_EARLY_RESULT);
|
|
1507
|
-
} else if (hasDynRef && _isCodegen && jsFn) {
|
|
1508
|
-
// $dynamicRef with JS codegen: direct path, no wrapper layers
|
|
1509
|
-
const _fn = jsFn, _efn = errOnly, _R = VALID_RESULT;
|
|
1510
|
-
this.validate = preprocess
|
|
1511
|
-
? (data) => { preprocess(data); return _fn(data) ? _R : _efn(data); }
|
|
1512
|
-
: (data) => _fn(data) ? _R : _efn(data);
|
|
1513
|
-
} else if (hasDynRef) {
|
|
1514
|
-
// $dynamicRef without codegen: the interpreted engine. It scores the
|
|
1515
|
-
// same on the suite's $dynamicRef cases as the native walker since the
|
|
1516
|
-
// dynamic-scope fix, needs no addon, and gets the verdict-only mode.
|
|
1517
|
-
if (!_interp) {
|
|
1518
|
-
const { createInterpreter } = require('./lib/interpreter');
|
|
1519
|
-
_interp = createInterpreter(schemaObj, {
|
|
1520
|
-
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1521
|
-
formats: this._userFormats,
|
|
1522
|
-
v1: isV1Dialect(schemaObj),
|
|
1523
|
-
keywords: this._keywords,
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
const interp = _interp;
|
|
1527
|
-
this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
|
|
1528
|
-
this.validate = preprocess
|
|
1529
|
-
? (data) => { preprocess(data); return interp.validate(data); }
|
|
1530
|
-
: (data) => interp.validate(data);
|
|
1531
|
-
} else if (jsFn && jsFn._hybridFactory) {
|
|
1532
|
-
// Zero-wrapper: hybridFactory bakes VALID_RESULT + errFn into a single function
|
|
1533
|
-
// No arrow function wrapper, no ternary, one function call
|
|
1534
|
-
// The factory bakes the error function in as an argument and never
|
|
1535
|
-
// calls it for a document that passes, so a resolver here costs the
|
|
1536
|
-
// accepted path nothing and keeps the compile off the first call.
|
|
1537
|
-
// Until the first rejection this is the hybrid: the verdict function,
|
|
1538
|
-
// with the error resolver baked in and never called for a document that
|
|
1539
|
-
// passes. That keeps the combined function's compile off the first call,
|
|
1540
|
-
// which is two thirds of what a first call costs.
|
|
1541
|
-
//
|
|
1542
|
-
// From the first rejection on, the combined function answers directly.
|
|
1543
|
-
// It decides and collects in one pass, so the verdict pass in front of it
|
|
1544
|
-
// was validating the document a second time: 134.1 microseconds against
|
|
1545
|
-
// its 45.3 on a 1000-user array. Swapping rather than starting there keeps
|
|
1546
|
-
// the lazy compile, and swapping at all is only free because the combined
|
|
1547
|
-
// function now costs what the verdict function costs on accepted
|
|
1548
|
-
// documents (1.02x on that array, 0.99x on a small body) since
|
|
1549
|
-
// additionalProperties stopped materialising its key array. The
|
|
1550
|
-
// indirection this needs measured inside the noise at both sizes.
|
|
1551
|
-
let impl = null;
|
|
1552
|
-
const onReject = (data) => {
|
|
1553
|
-
const combined = combinedIfSafe();
|
|
1554
|
-
if (combined) { impl = combined; return _mustReject(combined(data)); }
|
|
1555
|
-
return errOnly(data);
|
|
1556
|
-
};
|
|
1557
|
-
// Tiered: the first calls go through the verdict function and the
|
|
1558
|
-
// resolver, and the hybrid is compiled once the validator is in use.
|
|
1559
|
-
// Compiling it up front was a second parse of the whole schema on
|
|
1560
|
-
// every validator, most of which answer a handful of requests or only
|
|
1561
|
-
// verdicts. A refused compile stays on the first tier.
|
|
1562
|
-
let warm = 0;
|
|
1563
|
-
const first = (data) => {
|
|
1564
|
-
if (++warm >= HYBRID_TIER_CALLS && impl === first) {
|
|
1565
|
-
impl = jsFn._hybridFactory(VALID_RESULT, onReject) || ((d) => (jsFn(d) ? VALID_RESULT : onReject(d)));
|
|
1566
|
-
return impl(data);
|
|
1567
|
-
}
|
|
1568
|
-
return jsFn(data) ? VALID_RESULT : onReject(data);
|
|
1569
|
-
};
|
|
1570
|
-
impl = first;
|
|
1571
|
-
const run = (data) => impl(data);
|
|
1572
|
-
this.validate = preprocess
|
|
1573
|
-
? (data) => { preprocess(data); return run(data); }
|
|
1574
|
-
: run;
|
|
1575
|
-
} else {
|
|
1576
|
-
// No hybrid factory, so the assembly needs the function itself rather
|
|
1577
|
-
// than a reference it can call later: build it now.
|
|
1578
|
-
const safeCombinedFn = combinedIfSafe();
|
|
1579
|
-
if (safeCombinedFn) {
|
|
1580
|
-
this.validate = preprocess
|
|
1581
|
-
? (data) => { preprocess(data); return safeCombinedFn(data); }
|
|
1582
|
-
: safeCombinedFn;
|
|
1583
|
-
} else {
|
|
1584
|
-
this.validate = preprocess
|
|
1585
|
-
? (data) => {
|
|
1586
|
-
preprocess(data);
|
|
1587
|
-
return jsFn(data) ? VALID_RESULT : errOnly(data);
|
|
1588
|
-
}
|
|
1589
|
-
: (data) => (jsFn(data) ? VALID_RESULT : errOnly(data));
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
// Verbose mode: populate parentSchema, schema and data on each error, the
|
|
1593
|
-
// three fields the default error shape carries under the same option.
|
|
1594
|
-
// `data` is the value the error points at; without it a caller has to
|
|
1595
|
-
// walk the document by the instance path itself, which is what one
|
|
1596
|
-
// migration ended up writing by hand. Errors may be frozen, so clone
|
|
1597
|
-
// them with the extra fields.
|
|
1598
|
-
if (this._verbose) {
|
|
1599
|
-
const inner = this.validate;
|
|
1600
|
-
const root = this._schemaObj;
|
|
1601
|
-
const { resolvePointer } = require('./lib/pointer.js');
|
|
1602
|
-
this.validate = (data) => {
|
|
1603
|
-
const result = inner(data);
|
|
1604
|
-
if (result && !result.valid && result.errors) {
|
|
1605
|
-
const enriched = result.errors.map((err) => {
|
|
1606
|
-
if (!err || err.parentSchema !== undefined) return err;
|
|
1607
|
-
const parentSchema = resolveSchemaByPath(root, err.schemaPath);
|
|
1608
|
-
// The last segment of the schema path is the keyword that
|
|
1609
|
-
// failed, so its value on the parent is that keyword's schema.
|
|
1610
|
-
const sp = typeof err.schemaPath === 'string' ? err.schemaPath : '';
|
|
1611
|
-
const last = sp.slice(sp.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
1612
|
-
const keywordSchema = (parentSchema !== null && typeof parentSchema === 'object' && last)
|
|
1613
|
-
? parentSchema[last]
|
|
1614
|
-
: undefined;
|
|
1615
|
-
return {
|
|
1616
|
-
...err,
|
|
1617
|
-
parentSchema,
|
|
1618
|
-
schema: keywordSchema,
|
|
1619
|
-
data: resolvePointer(data, err.instancePath, undefined),
|
|
1620
|
-
};
|
|
1621
|
-
});
|
|
1622
|
-
return { valid: false, errors: enriched };
|
|
1623
|
-
}
|
|
1624
|
-
return result;
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
// The verdict methods answer validate()'s question without building the
|
|
1628
|
-
// error list, so they run the same preprocess pass. Skipping it made the
|
|
1629
|
-
// two disagree on input that coercion or a default would have fixed.
|
|
1630
|
-
_bindVerdict(this, fusedRemove
|
|
1631
|
-
? (data) => fusedRemove(data) || (preprocess(data), jsFn(data))
|
|
1632
|
-
: preprocess
|
|
1633
|
-
? (data) => { preprocess(data); return jsFn(data) }
|
|
1634
|
-
: jsFn);
|
|
1635
|
-
// Same preference as the object path: the combined function first, since
|
|
1636
|
-
// it validates and collects in one pass, and the error generator behind
|
|
1637
|
-
// it. `errPreferCombined` is that order, resolved on the first rejection
|
|
1638
|
-
// instead of at compile time.
|
|
1639
|
-
// Tiered the same way as validate() above.
|
|
1640
|
-
let jsonHybrid = null;
|
|
1641
|
-
let jsonWarm = 0;
|
|
1642
|
-
const jsonValidateInner = (obj) => {
|
|
1643
|
-
if (jsonHybrid !== null) return jsonHybrid(obj);
|
|
1644
|
-
if (++jsonWarm >= HYBRID_TIER_CALLS) {
|
|
1645
|
-
jsonHybrid = (jsFn._hybridFactory && jsFn._hybridFactory(VALID_RESULT, errPreferCombined))
|
|
1646
|
-
|| ((o) => (jsFn(o) ? VALID_RESULT : errPreferCombined(o)));
|
|
1647
|
-
return jsonHybrid(obj);
|
|
1648
|
-
}
|
|
1649
|
-
return jsFn(obj) ? VALID_RESULT : errPreferCombined(obj);
|
|
1650
|
-
};
|
|
1651
|
-
// Parsed text takes the same preprocess pass as a parsed object, so
|
|
1652
|
-
// validate(obj) and validateJSON(text) answer the same for the same
|
|
1653
|
-
// document. Without it, coercion, removal and defaults applied on one
|
|
1654
|
-
// path and not the other.
|
|
1655
|
-
const jsonValidateFn = preprocess
|
|
1656
|
-
? (obj) => { preprocess(obj); return jsonValidateInner(obj) }
|
|
1657
|
-
: jsonValidateInner;
|
|
1658
|
-
this.validateJSON = useSimdjsonForLarge && native && !preprocess
|
|
1659
|
-
? (jsonStr) => {
|
|
1660
|
-
// `_skipNativeFast` is set by the scanner short-circuit below when it
|
|
1661
|
-
// has already decided the document is invalid. The encode and the
|
|
1662
|
-
// native call would run only to return false, and the error path
|
|
1663
|
-
// underneath does not need them.
|
|
1664
|
-
if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
|
|
1665
|
-
this._ensureNative();
|
|
1666
|
-
const buf = Buffer.from(jsonStr);
|
|
1667
|
-
if (native.rawFastValidate(this._fastSlot, buf))
|
|
1668
|
-
return VALID_RESULT;
|
|
1669
|
-
return this._compiled.validateJSON(jsonStr);
|
|
1670
|
-
}
|
|
1671
|
-
try {
|
|
1672
|
-
return jsonValidateFn(JSON.parse(jsonStr));
|
|
1673
|
-
} catch (e) {
|
|
1674
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
1675
|
-
}
|
|
1676
|
-
this._ensureNative();
|
|
1677
|
-
return this._compiled.validateJSON(jsonStr);
|
|
1678
|
-
}
|
|
1679
|
-
: (jsonStr) => {
|
|
1680
|
-
try {
|
|
1681
|
-
return jsonValidateFn(JSON.parse(jsonStr));
|
|
1682
|
-
} catch (e) {
|
|
1683
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
1684
|
-
if (!native) return { valid: false, errors: [{ keyword: 'syntax', instancePath: '', schemaPath: '#', params: {}, message: e.message }] };
|
|
1685
|
-
}
|
|
1686
|
-
this._ensureNative();
|
|
1687
|
-
return this._compiled.validateJSON(jsonStr);
|
|
1688
|
-
};
|
|
1689
|
-
// The addon validates the bytes as they are, which is the wrong answer
|
|
1690
|
-
// when the schema asks for coercion, removal or defaults: those change
|
|
1691
|
-
// what counts as valid. With a preprocess pass configured the text is
|
|
1692
|
-
// parsed and run through the same path validate() takes.
|
|
1693
|
-
const verdictFromText = (jsonStr) => {
|
|
1694
|
-
let parsed;
|
|
1695
|
-
try {
|
|
1696
|
-
parsed = JSON.parse(jsonStr);
|
|
1697
|
-
} catch (e) {
|
|
1698
|
-
if (!(e instanceof SyntaxError)) throw e;
|
|
1699
|
-
return false;
|
|
1700
|
-
}
|
|
1701
|
-
if (preprocess) preprocess(parsed);
|
|
1702
|
-
return jsFn(parsed);
|
|
1703
|
-
};
|
|
1704
|
-
this.isValidJSON = useSimdjsonForLarge && native && !preprocess
|
|
1705
|
-
? (jsonStr) => {
|
|
1706
|
-
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
1707
|
-
this._ensureNative();
|
|
1708
|
-
return native.rawFastValidate(
|
|
1709
|
-
this._fastSlot,
|
|
1710
|
-
Buffer.from(jsonStr),
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
return verdictFromText(jsonStr);
|
|
1714
|
-
}
|
|
1715
|
-
: verdictFromText;
|
|
1716
|
-
|
|
1717
|
-
// A schema-directed scanner answers the verdict from the JSON text
|
|
1718
|
-
// without building the document. Parsing is around three quarters of the
|
|
1719
|
-
// cost of a real request, and a caller that only wants yes or no should
|
|
1720
|
-
// not pay it; a rejection can also stop at the byte that caused it
|
|
1721
|
-
// instead of parsing the rest of a document that is already refused.
|
|
1722
|
-
//
|
|
1723
|
-
// It is wired only where the verdict IS the answer. On a path that has
|
|
1724
|
-
// to produce errors, scanning an invalid document is work thrown away,
|
|
1725
|
-
// so those keep parsing. `abortEarly` has no errors to produce, so it
|
|
1726
|
-
// counts as a verdict path.
|
|
1727
|
-
//
|
|
1728
|
-
// Not wired when a preprocess pass is configured: coercion, removal and
|
|
1729
|
-
// defaults rewrite the document before it is judged, and the scanner
|
|
1730
|
-
// reads what arrived. The compiler declines any schema it cannot answer
|
|
1731
|
-
// and a compiled scanner returns BAIL for a document shape it cannot
|
|
1732
|
-
// answer, and then the parse path below takes over unchanged.
|
|
1733
|
-
// validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
|
|
1734
|
-
// validate) so it works with or without the native addon and in browsers.
|
|
1735
|
-
{
|
|
1736
|
-
const self = this;
|
|
1737
|
-
this.validateAndParse = (jsonStr) => {
|
|
1738
|
-
let value;
|
|
1739
|
-
try {
|
|
1740
|
-
value = JSON.parse(typeof jsonStr === 'string' ? jsonStr : new TextDecoder().decode(jsonStr));
|
|
1741
|
-
} catch (e) {
|
|
1742
|
-
return { valid: false, value: undefined, errors: [{ code: 'ATA9001', message: 'invalid JSON: ' + e.message, keyword: '__parse__', instancePath: '', schemaPath: '', params: {} }] };
|
|
1743
|
-
}
|
|
1744
|
-
const r = self.validate(value);
|
|
1745
|
-
return { valid: r.valid, value, errors: r.errors };
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
// Buffer APIs: lazy native init — only compile native schema on first buffer call.
|
|
1749
|
-
// This keeps cold start fast (JS codegen only) for users who only use validate().
|
|
1750
|
-
if (native) {
|
|
1751
|
-
const self = this;
|
|
1752
|
-
this.isValid = (buf) => {
|
|
1753
|
-
self._ensureNative();
|
|
1754
|
-
const slot = self._fastSlot;
|
|
1755
|
-
self.isValid = slot < 0
|
|
1756
|
-
? (b) => self._slowBufferValid(b, 'isValid')
|
|
1757
|
-
: (b) => {
|
|
1758
|
-
if (typeof b === 'string') b = Buffer.from(b);
|
|
1759
|
-
else if (!(b instanceof Uint8Array)) throw new TypeError('isValid() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().');
|
|
1760
|
-
return native.rawFastValidate(slot, b);
|
|
1761
|
-
};
|
|
1762
|
-
return self.isValid(buf);
|
|
1763
|
-
};
|
|
1764
|
-
this.countValid = (ndjsonBuf) => {
|
|
1765
|
-
self._ensureNative();
|
|
1766
|
-
const slot = self._fastSlot;
|
|
1767
|
-
self.countValid = slot < 0
|
|
1768
|
-
? (b) => {
|
|
1769
|
-
if (typeof b !== 'string' && !(b instanceof Uint8Array)) throw new TypeError('countValid() requires a Buffer, Uint8Array, or string');
|
|
1770
|
-
const text = typeof b === 'string' ? b : Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString('utf8');
|
|
1771
|
-
let c = 0;
|
|
1772
|
-
for (const line of text.split('\n')) {
|
|
1773
|
-
if (line.trim() === '') continue;
|
|
1774
|
-
if (self._slowBufferValid(line, 'countValid')) c++;
|
|
1775
|
-
}
|
|
1776
|
-
return c;
|
|
1777
|
-
}
|
|
1778
|
-
: (b) => {
|
|
1779
|
-
if (typeof b === 'string') b = Buffer.from(b);
|
|
1780
|
-
else if (!(b instanceof Uint8Array)) throw new TypeError('countValid() requires a Buffer, Uint8Array, or string');
|
|
1781
|
-
const r = native.rawNDJSONValidate(slot, b);
|
|
1782
|
-
let c = 0;
|
|
1783
|
-
for (let i = 0; i < r.length; i++) if (r[i]) c++;
|
|
1784
|
-
return c;
|
|
1785
|
-
};
|
|
1786
|
-
return self.countValid(ndjsonBuf);
|
|
1787
|
-
};
|
|
1788
|
-
this.batchIsValid = (buffers) => {
|
|
1789
|
-
self._ensureNative();
|
|
1790
|
-
const slot = self._fastSlot;
|
|
1791
|
-
self.batchIsValid = slot < 0
|
|
1792
|
-
? (bufs) => {
|
|
1793
|
-
let v = 0;
|
|
1794
|
-
for (const b of bufs) {
|
|
1795
|
-
if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
|
|
1796
|
-
if (self._slowBufferValid(b, 'batchIsValid')) v++;
|
|
1797
|
-
}
|
|
1798
|
-
return v;
|
|
1799
|
-
}
|
|
1800
|
-
: (bufs) => {
|
|
1801
|
-
let v = 0;
|
|
1802
|
-
for (const b of bufs) {
|
|
1803
|
-
if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
|
|
1804
|
-
if (native.rawFastValidate(slot, b)) v++;
|
|
1805
|
-
}
|
|
1806
|
-
return v;
|
|
1807
|
-
};
|
|
1808
|
-
return self.batchIsValid(buffers);
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
1811
|
-
} else if (native) {
|
|
1812
|
-
// No JS codegen: buffer/parallel APIs always come from the native
|
|
1813
|
-
// engine, but the object-validation entry points go to whichever
|
|
1814
|
-
// engine is more correct for the schema shape. Pure dynamic-ref
|
|
1815
|
-
// schemas stay on the C++ path (full $dynamicRef scope tracking);
|
|
1816
|
-
// everything else uses the interpreted engine, which handles the
|
|
1817
|
-
// $id/URN base-URI resolution corners the native resolver gets wrong.
|
|
1818
|
-
this._ensureNative();
|
|
1819
|
-
const _hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
|
|
1820
|
-
const _hasUneval = this._schemaStr.includes('"unevaluatedProperties"') || this._schemaStr.includes('"unevaluatedItems"')
|
|
1821
|
-
// propertyDependencies exists only in the interpreted engine, so a schema
|
|
1822
|
-
// using it goes there even when it also uses $dynamicRef.
|
|
1823
|
-
const _hasPropDeps = this._schemaStr.includes('"propertyDependencies"')
|
|
1824
|
-
// $dynamicRef used to delegate to the native validateJSON path here;
|
|
1825
|
-
// the interpreted engine now scores the same on those cases, carries
|
|
1826
|
-
// the verdict-only mode, and works without the addon.
|
|
1827
|
-
let _validate;
|
|
1828
|
-
{
|
|
1829
|
-
const { createInterpreter } = require('./lib/interpreter');
|
|
1830
|
-
const interp = createInterpreter(schemaObj, {
|
|
1831
|
-
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1832
|
-
formats: this._userFormats,
|
|
1833
|
-
v1: isV1Dialect(schemaObj),
|
|
1834
|
-
keywords: this._keywords,
|
|
1835
|
-
});
|
|
1836
|
-
this._engine = 'interpreter';
|
|
1837
|
-
_validate = (data) => interp.validate(data);
|
|
1838
|
-
this._fastVerdict = preprocess ? null : (d) => interp.isValid(d);
|
|
1839
|
-
this.validateJSON = (jsonStr) => {
|
|
1840
|
-
try {
|
|
1841
|
-
return _validate(JSON.parse(jsonStr));
|
|
1842
|
-
} catch (e) {
|
|
1843
|
-
return { valid: false, errors: [{ keyword: 'syntax', instancePath: '', schemaPath: '#', params: {}, message: e.message }] };
|
|
1844
|
-
}
|
|
1845
|
-
};
|
|
1846
|
-
this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
|
|
1847
|
-
}
|
|
1848
|
-
this.validate = preprocess
|
|
1849
|
-
? (data) => {
|
|
1850
|
-
preprocess(data);
|
|
1851
|
-
return _validate(data);
|
|
1852
|
-
}
|
|
1853
|
-
: _validate;
|
|
1854
|
-
_bindVerdict(this, this._fastVerdict
|
|
1855
|
-
? this._fastVerdict
|
|
1856
|
-
// The rewrite runs first here too, as it does for validate() above; a
|
|
1857
|
-
// verdict without it disagreed with validate() on input that coercion,
|
|
1858
|
-
// a default or a removed key would have fixed.
|
|
1859
|
-
: preprocess
|
|
1860
|
-
? (data) => { preprocess(data); return _validate(data).valid; }
|
|
1861
|
-
: (data) => _validate(data).valid);
|
|
1862
|
-
this.validateAndParse = (jsonStr) => this._compiled.validateAndParse(jsonStr);
|
|
1863
|
-
{
|
|
1864
|
-
const slot = this._fastSlot;
|
|
1865
|
-
this.isValid = (buf) => {
|
|
1866
|
-
if (typeof buf === 'string') buf = Buffer.from(buf);
|
|
1867
|
-
else if (!(buf instanceof Uint8Array)) throw new TypeError('isValid() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().');
|
|
1868
|
-
return native.rawFastValidate(slot, buf);
|
|
1869
|
-
};
|
|
1870
|
-
}
|
|
1871
|
-
{
|
|
1872
|
-
const slot = this._fastSlot;
|
|
1873
|
-
this.countValid = (ndjsonBuf) => {
|
|
1874
|
-
if (typeof ndjsonBuf === 'string') ndjsonBuf = Buffer.from(ndjsonBuf);
|
|
1875
|
-
else if (!(ndjsonBuf instanceof Uint8Array)) throw new TypeError('countValid() requires a Buffer, Uint8Array, or string');
|
|
1876
|
-
const results = native.rawNDJSONValidate(slot, ndjsonBuf);
|
|
1877
|
-
let count = 0;
|
|
1878
|
-
for (let i = 0; i < results.length; i++) if (results[i]) count++;
|
|
1879
|
-
return count;
|
|
1880
|
-
};
|
|
1881
|
-
}
|
|
1882
|
-
{
|
|
1883
|
-
const slot = this._fastSlot;
|
|
1884
|
-
this.batchIsValid = (buffers) => {
|
|
1885
|
-
let valid = 0;
|
|
1886
|
-
for (const buf of buffers) {
|
|
1887
|
-
if (!(buf instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
|
|
1888
|
-
if (native.rawFastValidate(slot, buf)) valid++;
|
|
1889
|
-
}
|
|
1890
|
-
return valid;
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
|
-
} else {
|
|
1894
|
-
// No JS codegen and no native engine: fall back to the interpreted
|
|
1895
|
-
// engine. Slow but correct, and strictly better than the previous
|
|
1896
|
-
// behavior (the lazy stubs re-dispatched to themselves forever).
|
|
1897
|
-
const { createInterpreter } = require('./lib/interpreter');
|
|
1898
|
-
const interp = createInterpreter(schemaObj, {
|
|
1899
|
-
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1900
|
-
formats: this._userFormats,
|
|
1901
|
-
v1: isV1Dialect(schemaObj),
|
|
1902
|
-
keywords: this._keywords,
|
|
1903
|
-
});
|
|
1904
|
-
this._engine = 'interpreter';
|
|
1905
|
-
if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
|
|
1906
|
-
// abortEarly is a documented contract, not a property of whichever engine
|
|
1907
|
-
// answered: it promises the frozen ATA9000 stub instead of a detailed
|
|
1908
|
-
// error, so code that branches on it has to behave the same with and
|
|
1909
|
-
// without code generation. Taking the verdict path here also skips
|
|
1910
|
-
// building the errors the caller said it did not want.
|
|
1911
|
-
const run = options.abortEarly
|
|
1912
|
-
? (preprocess
|
|
1913
|
-
? (data) => { preprocess(data); return interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
|
|
1914
|
-
: (data) => (interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT))
|
|
1915
|
-
: (preprocess
|
|
1916
|
-
? (data) => { preprocess(data); return interp.validate(data); }
|
|
1917
|
-
: (data) => interp.validate(data));
|
|
1918
|
-
this.validate = run;
|
|
1919
|
-
_bindVerdict(this, this._fastVerdict
|
|
1920
|
-
? this._fastVerdict
|
|
1921
|
-
: (data) => run(data).valid);
|
|
1922
|
-
this.validateJSON = (jsonStr) => {
|
|
1923
|
-
try {
|
|
1924
|
-
return run(JSON.parse(jsonStr));
|
|
1925
|
-
} catch (e) {
|
|
1926
|
-
return { valid: false, errors: [{ keyword: 'syntax', instancePath: '', schemaPath: '#', params: {}, message: e.message }] };
|
|
1927
|
-
}
|
|
1928
|
-
};
|
|
1929
|
-
this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
// Error presentation, one lazy layer: declaration-order sorting, rich
|
|
1933
|
-
// enrichment (received value, suggestions, source frames, docUrl), or the
|
|
1934
|
-
// raw v0.14 shape under `richErrors: false`. All of it is work a caller
|
|
1935
|
-
// that only reads `.valid` never sees, so it runs on first access to
|
|
1936
|
-
// `.errors` and is cached. One wrapper, one allocation per rejection.
|
|
1937
|
-
if (this.validate) {
|
|
1938
|
-
const inner = this.validate;
|
|
1939
|
-
const enrich = this._richErrors ? require('./lib/enrich-error').enrich : null;
|
|
1940
|
-
const root = this._schemaObj;
|
|
1941
|
-
const self = this;
|
|
1942
|
-
this.validate = (data) => {
|
|
1943
|
-
const result = inner(data);
|
|
1944
|
-
// abortEarly returns the shared ATA9000 stub; preserve it as-is so the
|
|
1945
|
-
// perf fast path stays allocation-free and the documented code stays stable.
|
|
1946
|
-
if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
|
|
1947
|
-
// The raw input travels with the rejection when validateJSON set one.
|
|
1948
|
-
// The map it implies is built on first access to `.errors`, so a
|
|
1949
|
-
// caller reading only `.valid` does not pay for a document walk.
|
|
1950
|
-
const rawInput = enrich ? self._lastRawInput : null;
|
|
1951
|
-
// One instance of a class with prototype accessors. An object
|
|
1952
|
-
// literal with a getter here cost a closure plus an accessor
|
|
1953
|
-
// definition on every rejection, several hundred nanoseconds
|
|
1954
|
-
// before any error was read.
|
|
1955
|
-
return new RichRejection(result, data, rawInput, self, root, enrich);
|
|
1956
|
-
}
|
|
1957
|
-
return result;
|
|
1958
|
-
};
|
|
1959
|
-
|
|
1960
|
-
// validateJSON also enriches: set _lastRawInput so the position cache
|
|
1961
|
-
// can lazily build a map for dataFrame attachment. Only validateJSON
|
|
1962
|
-
// wires this — validate(data) takes a pre-parsed object, by design.
|
|
1963
|
-
if (this._richErrors && this.validateJSON) {
|
|
1964
|
-
const innerJson = this.validateJSON;
|
|
1965
|
-
this.validateJSON = (jsonStr) => {
|
|
1966
|
-
// The inner path reads _lastRawInput to hand the raw text to the
|
|
1967
|
-
// rejection it builds. Cleared as soon as it returns: the rejection
|
|
1968
|
-
// carries the text itself, so nothing outlives the call.
|
|
1969
|
-
this._lastRawInput = jsonStr;
|
|
1970
|
-
let result;
|
|
1971
|
-
try {
|
|
1972
|
-
result = innerJson(jsonStr);
|
|
1973
|
-
} finally {
|
|
1974
|
-
this._lastRawInput = null;
|
|
1975
|
-
}
|
|
1976
|
-
// Every diagnostic the text path adds is deferred. Deciding here
|
|
1977
|
-
// whether there is anything to add would mean reading `result.errors`,
|
|
1978
|
-
// and on the codegen path that realizes the inner lazy layer, which is
|
|
1979
|
-
// the document walk this exists to avoid.
|
|
1980
|
-
if (result && result.valid === false) {
|
|
1981
|
-
return new LazyJsonRejection(result, jsonStr, this, enrich);
|
|
1982
|
-
}
|
|
1983
|
-
return result;
|
|
1984
|
-
};
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
// validate() resolves a typed `data` on success: the validated input, after
|
|
1989
|
-
// any in-place coercion/defaults. This matches the ValidationResult<T>
|
|
1990
|
-
// contract. isValidObject() and abortEarly stay allocation-free for hot
|
|
1991
|
-
// paths that only need a boolean.
|
|
1992
|
-
if (this.validate) {
|
|
1993
|
-
const _bare = this.validate;
|
|
1994
|
-
this.validate = fusedRemove
|
|
1995
|
-
// A document the removing verdict accepts is answered here, one call
|
|
1996
|
-
// deep; anything else takes the full path, see fusedRemove above.
|
|
1997
|
-
? (data) => {
|
|
1998
|
-
if (fusedRemove(data)) return { valid: true, data, errors: VALID_RESULT.errors };
|
|
1999
|
-
const r = _bare(data);
|
|
2000
|
-
return (r.valid === true && r.data === undefined)
|
|
2001
|
-
? { valid: true, data, errors: r.errors }
|
|
2002
|
-
: r;
|
|
2003
|
-
}
|
|
2004
|
-
: (data) => {
|
|
2005
|
-
const r = _bare(data);
|
|
2006
|
-
return (r.valid === true && r.data === undefined)
|
|
2007
|
-
? { valid: true, data, errors: r.errors }
|
|
2008
|
-
: r;
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
// Custom error messages: if any subschema declares an `errorMessage`
|
|
2013
|
-
// keyword, install an outermost decorator that overrides the `message`
|
|
2014
|
-
// field of the errors it owns. Gated on a one-time scan so schemas without
|
|
2015
|
-
// errorMessage keep the validate hot path untouched. Layered after rich
|
|
2016
|
-
// enrichment so `code`/`keyword`/`path` are already final and only the
|
|
2017
|
-
// human-facing message changes.
|
|
2018
|
-
{
|
|
2019
|
-
const emLib = require('./lib/error-messages');
|
|
2020
|
-
const schemaStr = this._schemaStr || (this._schemaObj ? JSON.stringify(this._schemaObj) : '');
|
|
2021
|
-
if (emLib.schemaHasErrorMessages(schemaStr)) {
|
|
2022
|
-
const root = this._schemaObj;
|
|
2023
|
-
const wrap = (inner) => (arg) => {
|
|
2024
|
-
const result = inner(arg);
|
|
2025
|
-
if (result && result.valid === false && result.errors && result.errors.length && result !== ABORT_EARLY_RESULT) {
|
|
2026
|
-
const overridden = emLib.applyErrorMessages(result.errors, root);
|
|
2027
|
-
if (overridden !== result.errors) return { valid: false, errors: overridden };
|
|
2028
|
-
}
|
|
2029
|
-
return result;
|
|
2030
|
-
};
|
|
2031
|
-
if (this.validate) this.validate = wrap(this.validate);
|
|
2032
|
-
if (this.validateJSON) this.validateJSON = wrap(this.validateJSON);
|
|
2033
|
-
// validateAndParse routes through self.validate on the codegen path, but
|
|
2034
|
-
// the native-only path returns directly from the addon — wrap it so both
|
|
2035
|
-
// paths get overrides. The result shape carries `value`, preserved here.
|
|
2036
|
-
if (this.validateAndParse) {
|
|
2037
|
-
const innerVP = this.validateAndParse;
|
|
2038
|
-
this.validateAndParse = (arg) => {
|
|
2039
|
-
const result = innerVP(arg);
|
|
2040
|
-
if (result && result.valid === false && result.errors && result.errors.length) {
|
|
2041
|
-
const overridden = emLib.applyErrorMessages(result.errors, root);
|
|
2042
|
-
if (overridden !== result.errors) return { valid: false, value: result.value, errors: overridden };
|
|
2043
|
-
}
|
|
2044
|
-
return result;
|
|
2045
|
-
};
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// Errors are paid for when read, not when produced. The full pipeline
|
|
2051
|
-
// above (error codegen, enrichment, custom messages, verbose) stays
|
|
2052
|
-
// intact, but validate() now answers the verdict from the boolean
|
|
2053
|
-
// engine and materializes `errors` through a getter on first access.
|
|
2054
|
-
// A caller that only reads `.valid`, which is every gateway check and
|
|
2055
|
-
// every benchmark, skips error construction entirely; a caller that
|
|
2056
|
-
// reads `.errors` pays once and the result is cached. Skipped when the
|
|
2057
|
-
// schema coerces or defaults (preprocess mutates before the verdict),
|
|
2058
|
-
// under abortEarly (already a frozen stub), and for $dynamicRef (the
|
|
2059
|
-
// boolean engine is not the authority there).
|
|
2060
|
-
// A check registered through _extendValidate joins here when it can: the
|
|
2061
|
-
// verdict comes from the generated function with the check compiled in,
|
|
2062
|
-
// and the check's own errors are appended when somebody reads them. A
|
|
2063
|
-
// rejection then costs what it costs without the check. Where this layer
|
|
2064
|
-
// is not installed, the end of this method wraps validate() instead.
|
|
2065
|
-
const _vx = this._validateTail !== null ? this._validateTail() : null;
|
|
2066
|
-
let _vxApplied = false;
|
|
2067
|
-
if (this._fastVerdict && !preprocess && !options.abortEarly && this.validate) {
|
|
2068
|
-
const _full = this.validate;
|
|
2069
|
-
const _fast = _vx ? _fuseTail(this._fastVerdict, _vx.check) : this._fastVerdict;
|
|
2070
|
-
const _extra = _vx ? _vx.errors : null;
|
|
2071
|
-
_vxApplied = true;
|
|
2072
|
-
const EMPTY_ERRORS = Object.freeze([]);
|
|
2073
|
-
const _verdictFallback = [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
|
|
2074
|
-
const _withExtra = (own, data) => {
|
|
2075
|
-
const more = _extra === null ? null : _extra(data);
|
|
2076
|
-
if (more && more.length) return own ? own.concat(more) : more;
|
|
2077
|
-
// The data changed between the verdict and this read; keep the
|
|
2078
|
-
// verdict and say so rather than inventing a specific error.
|
|
2079
|
-
return own || _verdictFallback;
|
|
2080
|
-
};
|
|
2081
|
-
const _buildErrors = (data) => {
|
|
2082
|
-
const r = _full(data);
|
|
2083
|
-
return _withExtra((r && r.valid === false && r.errors && r.errors.length) ? r.errors : null, data);
|
|
2084
|
-
};
|
|
2085
|
-
const _buildRawErrors = (data) => {
|
|
2086
|
-
const r = _full(data);
|
|
2087
|
-
let raw = null;
|
|
2088
|
-
if (r && r.valid === false) {
|
|
2089
|
-
raw = typeof r._ataRaw === 'function' ? r._ataRaw() : r.errors;
|
|
2090
|
-
if (!raw || !raw.length) raw = null;
|
|
2091
|
-
}
|
|
2092
|
-
return _withExtra(raw, data);
|
|
2093
|
-
};
|
|
2094
|
-
this.validate = (data) => {
|
|
2095
|
-
if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
|
|
2096
|
-
return new LazyRejection(_buildErrors, data, _buildRawErrors);
|
|
2097
|
-
};
|
|
2098
|
-
}
|
|
2099
|
-
if (_vx && !_vxApplied && this.validate) {
|
|
2100
|
-
const inner = this.validate;
|
|
2101
|
-
const { check, errors } = _vx;
|
|
2102
|
-
this.validate = (data) => {
|
|
2103
|
-
const r = inner(data);
|
|
2104
|
-
if (r.valid && check(data)) return r;
|
|
2105
|
-
return new ExtendedRejection(r, data, errors);
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
// The buffer APIs answer from the native walker, which disagrees with
|
|
2110
|
-
// validate() on shapes listed in lib/buffer-gate.js. For those schemas
|
|
2111
|
-
// every buffer entry point goes through validate() instead.
|
|
2112
|
-
if (native) {
|
|
2113
|
-
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
|
|
2114
|
-
if (bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords)) installSlowBufferApis(this);
|
|
2115
|
-
}
|
|
2116
|
-
// Installed after the buffer gate on purpose: the gate replaces
|
|
2117
|
-
// isValidJSON for schemas whose shapes the native walker gets wrong,
|
|
2118
|
-
// unevaluatedProperties among them, and the scanner wiring has to wrap
|
|
2119
|
-
// whatever answers last or a gated schema silently loses its scanner.
|
|
2120
|
-
if (this._jsFn && !this._preprocess) {
|
|
2121
|
-
const self = this;
|
|
2122
|
-
// Generating a scanner costs about 20 microseconds, measured, and it
|
|
2123
|
-
// saves from around 85 nanoseconds on a small accepted document to
|
|
2124
|
-
// several microseconds on a rejected one. Building it on the first
|
|
2125
|
-
// call would therefore be a straight loss for a caller that checks one
|
|
2126
|
-
// document and exits, so it is built once a caller has asked often
|
|
2127
|
-
// enough that it is plainly doing this in a loop. A server passes the
|
|
2128
|
-
// line during warm-up and never sees it.
|
|
2129
|
-
const SCAN_AFTER = 64;
|
|
2130
|
-
let calls = 0;
|
|
2131
|
-
// undefined: not built. null: this schema has no scanner. Passing true
|
|
2132
|
-
// builds it now, which is how the differential test reaches it.
|
|
2133
|
-
this._ensureScanner = (now) => {
|
|
2134
|
-
if (self._scanner === undefined) {
|
|
2135
|
-
if (!now && ++calls < SCAN_AFTER) return undefined;
|
|
2136
|
-
const built = require('./lib/scan-compiler').compileScanner(schemaObj, { userFormats: self._userFormats });
|
|
2137
|
-
self._scanner = built ? built.scan : null;
|
|
2138
|
-
}
|
|
2139
|
-
return self._scanner;
|
|
2140
|
-
};
|
|
2141
|
-
const byParsing = this.isValidJSON;
|
|
2142
|
-
// The verdict is a pure function of the text, and the caller a
|
|
2143
|
-
// gateway or a drift monitor keeps asking about is usually the same
|
|
2144
|
-
// text: a config file re-read on a timer, a heartbeat body. One
|
|
2145
|
-
// remembered (text, verdict) pair answers that case with a native
|
|
2146
|
-
// string compare, which is a memcmp, instead of a scan. Withheld when
|
|
2147
|
-
// user formats or custom keywords are present, since those are user
|
|
2148
|
-
// functions and nothing guarantees they are pure.
|
|
2149
|
-
const memoizable = !self._userFormats && !self._usesKeywords;
|
|
2150
|
-
let _memoText = null;
|
|
2151
|
-
let _memoVerdict = false;
|
|
2152
|
-
this.isValidJSON = (jsonStr) => {
|
|
2153
|
-
const scan = self._ensureScanner();
|
|
2154
|
-
if (scan === undefined) return byParsing(jsonStr);
|
|
2155
|
-
if (scan === null) { _bindEntry(self, 'isValidJSON', byParsing); return byParsing(jsonStr); }
|
|
2156
|
-
_bindEntry(self, 'isValidJSON', memoizable
|
|
2157
|
-
? (text) => {
|
|
2158
|
-
if (typeof text !== 'string') return byParsing(text);
|
|
2159
|
-
if (text === _memoText) return _memoVerdict;
|
|
2160
|
-
const r = scan(text);
|
|
2161
|
-
const verdict = r === -1 ? byParsing(text) : r === 1;
|
|
2162
|
-
_memoText = text;
|
|
2163
|
-
_memoVerdict = verdict;
|
|
2164
|
-
return verdict;
|
|
2165
|
-
}
|
|
2166
|
-
: (text) => {
|
|
2167
|
-
if (typeof text !== 'string') return byParsing(text);
|
|
2168
|
-
const r = scan(text);
|
|
2169
|
-
if (r === -1) return byParsing(text);
|
|
2170
|
-
return r === 1;
|
|
2171
|
-
});
|
|
2172
|
-
return self.isValidJSON(jsonStr);
|
|
2173
|
-
};
|
|
2174
|
-
// validateJSON gets the same short-circuit isValidJSON has. The verdict is
|
|
2175
|
-
// a property of the text, and the scanner reads the text once and
|
|
2176
|
-
// allocates nothing; above the simdjson threshold the path underneath
|
|
2177
|
-
// encoded the whole document to a Buffer and called the native validator
|
|
2178
|
-
// instead, which measured 340 microseconds against the scanner's 191 on a
|
|
2179
|
-
// 149 KB config, the same against the published addon as against a local
|
|
2180
|
-
// build.
|
|
2181
|
-
//
|
|
2182
|
-
// An accepted document stops at the scanner. A rejected one still has to
|
|
2183
|
-
// produce errors, so it goes on to the path below, which is the
|
|
2184
|
-
// rich-errors wrapper and everything under it; the flag only tells that
|
|
2185
|
-
// path to skip an encode and a native call that would return false. Doing
|
|
2186
|
-
// it the other way, returning errors from the inner function directly,
|
|
2187
|
-
// would hand back errors that never passed through enrichment.
|
|
2188
|
-
{
|
|
2189
|
-
const validateByParsing = this.validateJSON;
|
|
2190
|
-
const abortEarly = !!options.abortEarly;
|
|
2191
|
-
this.validateJSON = (jsonStr) => {
|
|
2192
|
-
const scan = self._ensureScanner();
|
|
2193
|
-
if (scan === undefined) return validateByParsing(jsonStr);
|
|
2194
|
-
if (scan === null) { _bindEntry(self, 'validateJSON', validateByParsing); return validateByParsing(jsonStr); }
|
|
2195
|
-
_bindEntry(self, 'validateJSON', (text) => {
|
|
2196
|
-
if (typeof text === 'string') {
|
|
2197
|
-
const r = scan(text);
|
|
2198
|
-
if (r === 1) return VALID_RESULT;
|
|
2199
|
-
if (r === 0) {
|
|
2200
|
-
if (abortEarly) return ABORT_EARLY_RESULT;
|
|
2201
|
-
self._skipNativeFast = true;
|
|
2202
|
-
try {
|
|
2203
|
-
return validateByParsing(text);
|
|
2204
|
-
} finally {
|
|
2205
|
-
self._skipNativeFast = false;
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
return validateByParsing(text);
|
|
2210
|
-
});
|
|
2211
|
-
return self.validateJSON(jsonStr);
|
|
2212
|
-
};
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
// An extension registered through _extendValidate covers the JSON entry
|
|
2217
|
-
// points too. They are final here except for the scanner stubs above,
|
|
2218
|
-
// which rebind themselves on first use through _bindEntry, so the
|
|
2219
|
-
// extension is applied to whatever each one is now and again on rebind.
|
|
2220
|
-
if (_vx) {
|
|
2221
|
-
this._entryExt = _jsonEntryWrappers(_vx);
|
|
2222
|
-
for (const name of ['validateJSON', 'isValidJSON', 'validateAndParse']) {
|
|
2223
|
-
if (Object.prototype.hasOwnProperty.call(this, name) && typeof this[name] === 'function') _bindEntry(this, name, this[name]);
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
_rememberInstance(this);
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
// Which engine answers validate() for this schema: 'codegen' (generated
|
|
2232
|
-
// JS), 'closure' (the closure compiler, the boolean fallback), 'native'
|
|
2233
|
-
// (the C++ engine, only for some $dynamicRef schemas), or 'interpreter'.
|
|
2234
|
-
// A diagnostic: the answer is the same on every engine, the cost is not.
|
|
2235
|
-
engine() {
|
|
2236
|
-
this._ensureCompiled();
|
|
2237
|
-
return this._engine || 'interpreter';
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
_ensureNative() {
|
|
2241
|
-
if (this._nativeReady) return;
|
|
2242
|
-
this._nativeReady = true;
|
|
2243
|
-
if (!native) return;
|
|
2244
|
-
let nativeSchemaStr = this._schemaStr;
|
|
2245
|
-
if (this._schemaMap.size > 0) {
|
|
2246
|
-
const merged = JSON.parse(this._schemaStr);
|
|
2247
|
-
if (!merged.$defs) merged.$defs = {};
|
|
2248
|
-
for (const [id, s] of this._schemaMap) {
|
|
2249
|
-
merged.$defs['__ext_' + id.replace(/[^a-zA-Z0-9]/g, '_')] = s;
|
|
2250
|
-
}
|
|
2251
|
-
nativeSchemaStr = JSON.stringify(merged);
|
|
2252
|
-
}
|
|
2253
|
-
this._compiled = new native.CompiledSchema(nativeSchemaStr);
|
|
2254
|
-
// The fast registry is a fixed array of slots in the addon, and registering
|
|
2255
|
-
// a distinct schema past the last one throws. It is an accelerator for the
|
|
2256
|
-
// buffer path, not a requirement, so a full registry leaves the slot at -1
|
|
2257
|
-
// and the buffer methods answer from the JS engine instead. Letting this
|
|
2258
|
-
// throw made every validator built after the 4096th unusable on that path.
|
|
2259
|
-
try {
|
|
2260
|
-
this._fastSlot = native.fastRegister(nativeSchemaStr);
|
|
2261
|
-
} catch {
|
|
2262
|
-
this._fastSlot = -1;
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
// The buffer path without a fast slot: decode, parse, and hand the value to
|
|
2267
|
-
// the engine that does not need one. Slower than the zero-copy walk, and the
|
|
2268
|
-
// same answer. A negative slot must never reach rawFastValidate, whose bounds
|
|
2269
|
-
// check would report a valid document as invalid.
|
|
2270
|
-
_slowBufferValid(input, who) {
|
|
2271
|
-
let text;
|
|
2272
|
-
if (typeof input === 'string') text = input;
|
|
2273
|
-
else if (input instanceof Uint8Array) {
|
|
2274
|
-
text = Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8');
|
|
2275
|
-
} else {
|
|
2276
|
-
throw new TypeError(who + '() requires a Buffer, Uint8Array, or string');
|
|
2277
|
-
}
|
|
2278
|
-
let parsed;
|
|
2279
|
-
try {
|
|
2280
|
-
parsed = JSON.parse(text);
|
|
2281
|
-
} catch {
|
|
2282
|
-
return false;
|
|
2283
|
-
}
|
|
2284
|
-
return this.isValidObject(parsed);
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
addSchema(schema) {
|
|
2288
|
-
if (this._initialized) {
|
|
2289
|
-
throw new Error('Cannot add schema after compilation — call addSchema() before validate()')
|
|
2290
|
-
}
|
|
2291
|
-
if (!schema || !schema.$id) {
|
|
2292
|
-
throw new Error('Schema must have $id')
|
|
2293
|
-
}
|
|
2294
|
-
// Normalize a copy so the caller's object is never mutated. A document
|
|
2295
|
-
// without a dialect of its own is read under the root's draft.
|
|
2296
|
-
const root = this._schemaObj
|
|
2297
|
-
const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
|
|
2298
|
-
(root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
|
|
2299
|
-
const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
|
|
2300
|
-
this._ownSchemaMap()
|
|
2301
|
-
this._schemaMap.set(normalized.$id, normalized)
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
// buildSchemaMap hands the same map to every validator built from the same
|
|
2305
|
-
// registry. Take a private copy before writing to it.
|
|
2306
|
-
_ownSchemaMap() {
|
|
2307
|
-
if (!this._schemaMapShared) return
|
|
2308
|
-
this._schemaMap = new Map(this._schemaMap)
|
|
2309
|
-
this._schemaMapShared = false
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
_ensureCodegen() {
|
|
2313
|
-
if (this._jsFn) return;
|
|
2314
|
-
// A validator that rewrites its input cannot use the binding below: that
|
|
2315
|
-
// one answers from the compiled function alone and would skip the rewrite,
|
|
2316
|
-
// so isValidObject() and validate() would disagree.
|
|
2317
|
-
if (this._needsPreprocess() || this._usesKeywords) {
|
|
2318
|
-
this._ensureCompiled();
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
this._ensureVocabularies();
|
|
2322
|
-
if (this._interpretOnly || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI)) return;
|
|
2323
|
-
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
2324
|
-
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
2325
|
-
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
2326
|
-
// Custom formats are JS functions: skip the shared cache so different
|
|
2327
|
-
// validators with the same schema string but different formats don't collide.
|
|
2328
|
-
const cached = (this._userFormats || this._usesKeywords) ? null : _compileCache.get(mapKey);
|
|
2329
|
-
if (cached && cached.jsFn) {
|
|
2330
|
-
this._jsFn = cached.jsFn;
|
|
2331
|
-
_bindVerdict(this, cached.jsFn);
|
|
2332
|
-
_rememberInstance(this);
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
const uf = this._userFormats;
|
|
2336
|
-
const _cg = compileToJSCodegen(this._schemaObj, sm, uf);
|
|
2337
|
-
const jsFn = _cg || compileToJS(this._schemaObj, null, sm);
|
|
2338
|
-
this._jsFn = jsFn;
|
|
2339
|
-
if (jsFn) {
|
|
2340
|
-
_bindVerdict(this, jsFn);
|
|
2341
|
-
_rememberInstance(this);
|
|
2342
|
-
// A partial entry: the verdict function is real, the other two are not
|
|
2343
|
-
// built yet rather than declined. `undefined` is the not-built marker
|
|
2344
|
-
// the full compile's _buildErr/_buildCombined look for; `null` would read as
|
|
2345
|
-
// "the compiler declined" and cost the schema its error function, which
|
|
2346
|
-
// is the bug this cache had once already. `isCodegen` rides along so a
|
|
2347
|
-
// validator that later reuses this entry reports the same engine it
|
|
2348
|
-
// would have compiled to.
|
|
2349
|
-
if (!uf) {
|
|
2350
|
-
if (!cached) _compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: !!_cg, full: false });
|
|
2351
|
-
else cached.jsFn = jsFn;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
// Load a pre-compiled standalone module. Zero schema compilation.
|
|
2357
|
-
// No NAPI, no native compile — pure JS. Startup in microseconds.
|
|
2358
|
-
// Usage: const v = Validator.fromStandalone(require('./compiled.js'), schema, opts)
|
|
2359
|
-
static fromStandalone(mod, schema, opts) {
|
|
2360
|
-
const options = opts || {};
|
|
2361
|
-
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
2362
|
-
|
|
2363
|
-
// Create a lightweight instance — skip NAPI compile entirely
|
|
2364
|
-
const v = Object.create(Validator.prototype);
|
|
2365
|
-
v._jsFn = mod.boolFn;
|
|
2366
|
-
v._compiled = null;
|
|
2367
|
-
v._fastSlot = -1;
|
|
2368
|
-
|
|
2369
|
-
// Mutators
|
|
2370
|
-
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
2371
|
-
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
2372
|
-
const applyRemove = options.removeAdditional
|
|
2373
|
-
? buildRemover(schemaObj)
|
|
2374
|
-
: null;
|
|
2375
|
-
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
2376
|
-
const preprocess =
|
|
2377
|
-
mutators.length === 0
|
|
2378
|
-
? null
|
|
2379
|
-
: mutators.length === 1
|
|
2380
|
-
? mutators[0]
|
|
2381
|
-
: (data) => {
|
|
2382
|
-
for (let i = 0; i < mutators.length; i++) mutators[i](data);
|
|
2383
|
-
};
|
|
2384
|
-
v._preprocess = preprocess;
|
|
2385
|
-
|
|
2386
|
-
// Error function — use pre-compiled from standalone if available, else compile
|
|
2387
|
-
let errFn = (d) => ({
|
|
2388
|
-
valid: false,
|
|
2389
|
-
errors: [
|
|
2390
|
-
{ code: "validation_failed", path: "", message: "validation failed" },
|
|
2391
|
-
],
|
|
2392
|
-
});
|
|
2393
|
-
if (mod.errFn) {
|
|
2394
|
-
errFn = (d) => mod.errFn(d, true);
|
|
2395
|
-
} else {
|
|
2396
|
-
const jsErrFn = compileToJSCodegenWithErrors(schemaObj);
|
|
2397
|
-
if (jsErrFn) {
|
|
2398
|
-
try {
|
|
2399
|
-
jsErrFn({}, true);
|
|
2400
|
-
errFn = (d) => jsErrFn(d, true);
|
|
2401
|
-
} catch {}
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
// Hybrid or speculative
|
|
2406
|
-
const hybridFn = mod.hybridFactory
|
|
2407
|
-
? mod.hybridFactory(VALID_RESULT, errFn)
|
|
2408
|
-
: null;
|
|
2409
|
-
|
|
2410
|
-
v.validate = hybridFn
|
|
2411
|
-
? preprocess
|
|
2412
|
-
? (data) => {
|
|
2413
|
-
preprocess(data);
|
|
2414
|
-
return hybridFn(data);
|
|
2415
|
-
}
|
|
2416
|
-
: hybridFn
|
|
2417
|
-
: preprocess
|
|
2418
|
-
? (data) => {
|
|
2419
|
-
preprocess(data);
|
|
2420
|
-
return mod.boolFn(data) ? VALID_RESULT : errFn(data);
|
|
2421
|
-
}
|
|
2422
|
-
: (data) => (mod.boolFn(data) ? VALID_RESULT : errFn(data));
|
|
2423
|
-
{
|
|
2424
|
-
const _bare = v.validate;
|
|
2425
|
-
v.validate = (data) => {
|
|
2426
|
-
const r = _bare(data);
|
|
2427
|
-
return (r.valid === true && r.data === undefined)
|
|
2428
|
-
? { valid: true, data, errors: r.errors }
|
|
2429
|
-
: r;
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
v.isValidObject = mod.boolFn;
|
|
2433
|
-
v.isValidJSON = (jsonStr) => {
|
|
2434
|
-
try {
|
|
2435
|
-
return mod.boolFn(JSON.parse(jsonStr));
|
|
2436
|
-
} catch {
|
|
2437
|
-
return false;
|
|
2438
|
-
}
|
|
2439
|
-
};
|
|
2440
|
-
v.validateJSON = (jsonStr) => {
|
|
2441
|
-
try {
|
|
2442
|
-
const obj = JSON.parse(jsonStr);
|
|
2443
|
-
return hybridFn
|
|
2444
|
-
? hybridFn(obj)
|
|
2445
|
-
: mod.boolFn(obj)
|
|
2446
|
-
? VALID_RESULT
|
|
2447
|
-
: errFn(obj);
|
|
2448
|
-
} catch {
|
|
2449
|
-
return {
|
|
2450
|
-
valid: false,
|
|
2451
|
-
errors: [{ code: "invalid_json", path: "", message: "invalid JSON" }],
|
|
2452
|
-
};
|
|
2453
|
-
}
|
|
2454
|
-
};
|
|
2455
|
-
|
|
2456
|
-
v.validateAndParse = native
|
|
2457
|
-
? (jsonStr) => {
|
|
2458
|
-
v._ensureNative();
|
|
2459
|
-
v.validateAndParse = (s) => v._compiled.validateAndParse(s);
|
|
2460
|
-
return v.validateAndParse(jsonStr);
|
|
2461
|
-
}
|
|
2462
|
-
: () => { throw new Error('Native addon required for validateAndParse()'); };
|
|
2463
|
-
|
|
2464
|
-
// Standard Schema V1
|
|
2465
|
-
Object.defineProperty(v, "~standard", {
|
|
2466
|
-
value: Object.freeze({
|
|
2467
|
-
version: 1,
|
|
2468
|
-
vendor: "ata-validator",
|
|
2469
|
-
validate(value) {
|
|
2470
|
-
const result = v.validate(value);
|
|
2471
|
-
if (result.valid) return { value };
|
|
2472
|
-
return {
|
|
2473
|
-
issues: result.errors.map((e) => ({
|
|
2474
|
-
message: e.message,
|
|
2475
|
-
path: parsePointerPath(e.instancePath),
|
|
2476
|
-
})),
|
|
2477
|
-
};
|
|
2478
|
-
},
|
|
2479
|
-
}),
|
|
2480
|
-
writable: false,
|
|
2481
|
-
enumerable: false,
|
|
2482
|
-
configurable: false,
|
|
2483
|
-
});
|
|
2484
|
-
|
|
2485
|
-
return v;
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
// Raw NAPI fast path for Buffer/Uint8Array
|
|
2489
|
-
isValid(input) {
|
|
2490
|
-
if (!native) throw new Error('Native addon required for isValid() — install build tools or use validate() instead');
|
|
2491
|
-
if (typeof input === 'string') input = Buffer.from(input);
|
|
2492
|
-
else if (!(input instanceof Uint8Array)) throw new TypeError('isValid() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().');
|
|
2493
|
-
this._ensureNative();
|
|
2494
|
-
return native.rawFastValidate(this._fastSlot, input);
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
// Zero-copy pre-padded path
|
|
2498
|
-
isValidPrepadded(paddedBuffer, jsonLength) {
|
|
2499
|
-
if (!native) throw new Error('Native addon required for isValidPrepadded()');
|
|
2500
|
-
this._ensureNative();
|
|
2501
|
-
return native.rawFastValidate(this._fastSlot, paddedBuffer, jsonLength);
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
// Parallel NDJSON batch (multi-core)
|
|
2505
|
-
isValidParallel(buffer) {
|
|
2506
|
-
if (!native) throw new Error('Native addon required for isValidParallel()');
|
|
2507
|
-
this._ensureNative();
|
|
2508
|
-
return native.rawParallelValidate(this._fastSlot, buffer);
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
// Parallel count (fastest -- single uint32 return)
|
|
2512
|
-
countValid(buffer) {
|
|
2513
|
-
if (!native) throw new Error('Native addon required for countValid()');
|
|
2514
|
-
this._ensureNative();
|
|
2515
|
-
return native.rawParallelCount(this._fastSlot, buffer);
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
// NDJSON single-thread batch
|
|
2519
|
-
isValidNDJSON(buffer) {
|
|
2520
|
-
if (!native) throw new Error('Native addon required for isValidNDJSON()');
|
|
2521
|
-
this._ensureNative();
|
|
2522
|
-
return native.rawNDJSONValidate(this._fastSlot, buffer);
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
// One-shot validate. It goes through a Validator like every other entry
|
|
2527
|
-
// point, so the result has one shape everywhere: `data` on success, errors
|
|
2528
|
-
// with a code, a keyword and an instancePath. From the first native binding
|
|
2529
|
-
// until 1.33.3 it handed the schema straight to the native engine whenever
|
|
2530
|
-
// the addon was loaded, which is every default install on a supported
|
|
2531
|
-
// platform, and returned that engine's raw result: numeric codes, `path`
|
|
2532
|
-
// instead of `instancePath`, no keyword, and no `data`. The compile cache keeps
|
|
2533
|
-
// a schema passed again from compiling again.
|
|
2534
|
-
function validate(schema, data) {
|
|
2535
|
-
if (schema instanceof Validator) return schema.validate(data);
|
|
2536
|
-
const v = new Validator(typeof schema === "string" ? JSON.parse(schema) : schema);
|
|
2537
|
-
return v.validate(data);
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
// Async validation for schemas built with `t.refine(...)`. Structural
|
|
2541
|
-
// validation runs synchronously first; refinements are awaited only when the
|
|
2542
|
-
// value is structurally valid (a refinement body may assume the right shape).
|
|
2543
|
-
// Accepts a schema literal or an existing Validator instance plus its schema.
|
|
2544
|
-
// Returns a Promise<ValidationResult>.
|
|
2545
|
-
async function validateAsync(schemaOrValidator, data) {
|
|
2546
|
-
const refineLib = require('./lib/refine');
|
|
2547
|
-
let validator, schema;
|
|
2548
|
-
if (schemaOrValidator instanceof Validator) {
|
|
2549
|
-
validator = schemaOrValidator;
|
|
2550
|
-
schema = validator._schemaObj;
|
|
2551
|
-
} else {
|
|
2552
|
-
schema = schemaOrValidator;
|
|
2553
|
-
validator = new Validator(schema);
|
|
2554
|
-
}
|
|
2555
|
-
const structural = validator.validate(data);
|
|
2556
|
-
if (!structural.valid) return structural;
|
|
2557
|
-
const refinements = refineLib.getRefinements(schema);
|
|
2558
|
-
if (!refinements) return structural;
|
|
2559
|
-
const issues = await refineLib.runRefinements(refinements, structural.data !== undefined ? structural.data : data);
|
|
2560
|
-
if (issues.length) return { valid: false, errors: issues };
|
|
2561
|
-
return structural;
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
// parseAsync resolves to the validated data, or rejects with an Error whose
|
|
2565
|
-
// `.errors` carries the ValidationError list. Mirrors the parse/validate split
|
|
2566
|
-
// used by Zod-style callers.
|
|
2567
|
-
async function parseAsync(schemaOrValidator, data) {
|
|
2568
|
-
const result = await validateAsync(schemaOrValidator, data);
|
|
2569
|
-
if (result.valid) return result.data !== undefined ? result.data : data;
|
|
2570
|
-
const err = new Error('ata: async validation failed');
|
|
2571
|
-
err.errors = result.errors;
|
|
2572
|
-
throw err;
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
function version() {
|
|
2576
|
-
if (native) return native.version();
|
|
2577
|
-
try { return require("./lib/version"); } catch { return "unknown"; }
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
// Static AOT entry points are thin lazy-loaders into `lib/aot.js`. The
|
|
2581
|
-
// implementation files (and the `fs`/`path` reads they perform) only enter
|
|
2582
|
-
// the process when one of these is actually called. See `lib/aot.js` for the
|
|
2583
|
-
// generated module shapes; browser bundles get `lib/aot.browser.js` (a stub
|
|
2584
|
-
// that throws) via the package.json `browser` field.
|
|
2585
|
-
Validator.bundle = function (schemas, opts) {
|
|
2586
|
-
return require('./lib/aot.js').bundle(Validator, schemas, opts);
|
|
2587
|
-
};
|
|
2588
|
-
|
|
2589
|
-
Validator.bundleStandalone = function (schemas, opts) {
|
|
2590
|
-
return require('./lib/aot.js').bundleStandalone(Validator, schemas, opts);
|
|
2591
|
-
};
|
|
2592
|
-
|
|
2593
|
-
Validator.bundleCompact = function (schemas, opts) {
|
|
2594
|
-
return require('./lib/aot.js').bundleCompact(Validator, schemas, opts);
|
|
2595
|
-
};
|
|
2596
|
-
|
|
2597
|
-
Validator.loadBundle = function (mods, schemas, opts) {
|
|
2598
|
-
return require('./lib/aot.js').loadBundle(Validator, mods, schemas, opts);
|
|
2599
|
-
};
|
|
2600
|
-
|
|
2601
|
-
const parseJSON = native ? native.parseJSON : JSON.parse;
|
|
2602
|
-
|
|
2603
|
-
// Ultra-fast compile: returns validate function directly, no Validator wrapper
|
|
2604
|
-
// WeakMap cached — second call with same schema object is ~3ns
|
|
2605
|
-
const _compileFnCache = new WeakMap();
|
|
2606
|
-
function compile(schema, opts) {
|
|
2607
|
-
if (!opts && typeof schema === 'object' && schema !== null) {
|
|
2608
|
-
const hit = _compileFnCache.get(schema);
|
|
2609
|
-
if (hit) return hit;
|
|
2610
|
-
}
|
|
2611
|
-
const v = new Validator(schema, opts);
|
|
2612
|
-
v._ensureCompiled();
|
|
2613
|
-
const fn = v.validate;
|
|
2614
|
-
if (!opts && typeof schema === 'object' && schema !== null) {
|
|
2615
|
-
_compileFnCache.set(schema, fn);
|
|
2616
|
-
}
|
|
2617
|
-
return fn;
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
const { toTypeScript } = require("./lib/ts-gen");
|
|
2621
|
-
const { renderPretty } = require("./lib/render-pretty");
|
|
2622
|
-
const { renderCompact } = require("./lib/render-compact");
|
|
2623
|
-
const { toOutput } = require("./lib/output-format");
|
|
2624
|
-
const { toRetryMessage } = require("./lib/retry-message");
|
|
2625
|
-
const { describeSchema } = require("./lib/describe-schema");
|
|
2626
|
-
const { renderJSON } = require("./lib/render-json");
|
|
2627
|
-
const { suggestFor } = require("./lib/suggestions");
|
|
2628
|
-
const { reprValue } = require("./lib/enrich-error");
|
|
2629
|
-
|
|
2630
|
-
// Walk a JSON pointer (RFC 6901 escapes) into a data tree. Mirrors the helper
|
|
2631
|
-
// inside lib/suggestions.js — kept local to avoid exporting an internal.
|
|
2632
|
-
function _walkPointer (root, pointer) {
|
|
2633
|
-
if (!pointer) return root;
|
|
2634
|
-
const parts = pointer.replace(/^\//, '').split('/').map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
2635
|
-
let cur = root;
|
|
2636
|
-
for (const p of parts) { if (cur == null) return undefined; cur = cur[p]; }
|
|
2637
|
-
return cur;
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
// Post-hoc suggestion enrichment for AOT-compiled validators. The standalone
|
|
2641
|
-
// modules do not embed the suggestion engine (Levenshtein + format hints would
|
|
2642
|
-
// inflate the gzipped bundle beyond the size budget). Consumers who want
|
|
2643
|
-
// suggestions pass the error array through this helper after validation.
|
|
2644
|
-
// AOT errors don't carry `received`, so we re-derive it from `data` here.
|
|
2645
|
-
const { setDiagnosticSource } = require('./lib/diagnostic-source');
|
|
2646
|
-
const attachDiagnosticSource = setDiagnosticSource;
|
|
2647
|
-
|
|
2648
|
-
// Resolved once. A require() inside the function was re-resolving the path
|
|
2649
|
-
// on every rejection, which the profile showed as internalModuleStat at the
|
|
2650
|
-
// top of the reject path, above the correlation it was loading.
|
|
2651
|
-
let _correlateTypos = null;
|
|
2652
|
-
function attachRelated (errors) {
|
|
2653
|
-
if (_correlateTypos === null) _correlateTypos = require('./lib/correlate').correlateTypos;
|
|
2654
|
-
const pairs = _correlateTypos(errors);
|
|
2655
|
-
if (pairs === null) return errors;
|
|
2656
|
-
for (const [from, to] of pairs) {
|
|
2657
|
-
const e = errors[from];
|
|
2658
|
-
if (!e) continue;
|
|
2659
|
-
if (e.related) { if (!e.related.includes(to)) e.related.push(to); }
|
|
2660
|
-
else e.related = [to];
|
|
2661
|
-
}
|
|
2662
|
-
return errors;
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
function attachSuggestions (errors, data) {
|
|
2666
|
-
if (!errors) return errors;
|
|
2667
|
-
for (const e of errors) {
|
|
2668
|
-
if (!e || e.suggestion) continue;
|
|
2669
|
-
let received = e.received;
|
|
2670
|
-
if (received === undefined && data !== undefined) {
|
|
2671
|
-
const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
|
|
2672
|
-
const raw = _walkPointer(data, ptr);
|
|
2673
|
-
if (raw !== undefined || ptr === '') received = reprValue(raw);
|
|
2674
|
-
}
|
|
2675
|
-
const probe = received !== undefined && e.received === undefined
|
|
2676
|
-
? Object.assign({}, e, { received })
|
|
2677
|
-
: e;
|
|
2678
|
-
const s = suggestFor(probe, data);
|
|
2679
|
-
if (s) e.suggestion = s;
|
|
2680
|
-
}
|
|
2681
|
-
// AOT modules import nothing, so this is their only route to a frame. The
|
|
2682
|
-
// caller holds the original object and ran no preprocessing through here.
|
|
2683
|
-
attachDiagnosticSource(errors, { data, mutatesInput: false });
|
|
2684
|
-
return errors;
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
// Authoring helper: identity at runtime. Its only job is to attach the
|
|
2688
|
-
// JSONSchema type (see index.d.ts) to an inline schema object so TypeScript
|
|
2689
|
-
// gives autocomplete and value checking while authoring. Returns the schema
|
|
2690
|
-
// untouched so it can be passed straight to Validator, toStandaloneModule, etc.
|
|
2691
|
-
function defineSchema (schema) {
|
|
2692
|
-
return schema;
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
// Public methods start as memoized accessors on the prototype. A fresh
|
|
2696
|
-
// Validator allocates none of them; the first read of a method builds the
|
|
2697
|
-
// bound closure, stores it on the instance as an ordinary writable property
|
|
2698
|
-
// and returns it. The setter keeps the compile step's plain assignments
|
|
2699
|
-
// (`this.validate = fn`) working before the getter has ever run. Detached
|
|
2700
|
-
// use (`const f = v.validate`) keeps working because the closure binds the
|
|
2701
|
-
// instance.
|
|
2702
|
-
// Standard Schema V1. Built on first read, then pinned to the instance with
|
|
2703
|
-
// the same descriptor the constructor used to install eagerly.
|
|
2704
|
-
Object.defineProperty(Validator.prototype, "~standard", {
|
|
2705
|
-
configurable: true,
|
|
2706
|
-
get() {
|
|
2707
|
-
const self = this;
|
|
2708
|
-
const std = Object.freeze({
|
|
2709
|
-
version: 1,
|
|
2710
|
-
vendor: "ata-validator",
|
|
2711
|
-
validate(value) {
|
|
2712
|
-
const result = self.validate(value);
|
|
2713
|
-
if (result.valid) {
|
|
2714
|
-
return { value };
|
|
2715
|
-
}
|
|
2716
|
-
// An issue carries a message and a path and nothing else, so the
|
|
2717
|
-
// suggestion and source-frame work the rich error path does would be
|
|
2718
|
-
// thrown away here. Take the raw, schema-ordered list when the result
|
|
2719
|
-
// offers one; fall back to the public list otherwise.
|
|
2720
|
-
const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
|
|
2721
|
-
const issues = new Array(raw.length);
|
|
2722
|
-
for (let i = 0; i < raw.length; i++) {
|
|
2723
|
-
const err = raw[i];
|
|
2724
|
-
const path = err.instancePath != null ? err.instancePath : (err.path || '');
|
|
2725
|
-
let message = err.message;
|
|
2726
|
-
if (!message) {
|
|
2727
|
-
// The native engine reports numeric codes without a message; the
|
|
2728
|
-
// enrich pass knows how to word those. Rare, so required lazily.
|
|
2729
|
-
message = require('./lib/enrich-error').enrich(err, {}).message;
|
|
2730
|
-
}
|
|
2731
|
-
issues[i] = { message, path: parsePointerPath(path) };
|
|
2732
|
-
}
|
|
2733
|
-
return { issues };
|
|
2734
|
-
},
|
|
2735
|
-
});
|
|
2736
|
-
Object.defineProperty(this, "~standard", {
|
|
2737
|
-
value: std,
|
|
2738
|
-
writable: false,
|
|
2739
|
-
enumerable: false,
|
|
2740
|
-
configurable: false,
|
|
2741
|
-
});
|
|
2742
|
-
return std;
|
|
2743
|
-
},
|
|
2744
|
-
});
|
|
2745
|
-
|
|
2746
|
-
// The error resolvers run only after a verdict function has said no. If one
|
|
2747
|
-
// answers valid anyway, two generators disagree, and the verdict is the one
|
|
2748
|
-
// to keep: returning the resolver's answer is how a vacuous combined function
|
|
2749
|
-
// turned a rejection into an acceptance in validateJSON. The disagreement is
|
|
2750
|
-
// reported as a generic failure rather than hidden.
|
|
2751
|
-
const _VERDICT_DISAGREES = Object.freeze({
|
|
2752
|
-
valid: false,
|
|
2753
|
-
errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
|
|
2754
|
-
});
|
|
2755
|
-
function _mustReject(r) {
|
|
2756
|
-
return r && r.valid === false ? r : _VERDICT_DISAGREES;
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
// Install the verdict method. Every place that binds isValidObject comes
|
|
2760
|
-
// through here, so a check registered with _extendVerdict survives the method
|
|
2761
|
-
// being replaced as the validator compiles further, which it does more than
|
|
2762
|
-
// once over its life.
|
|
2763
|
-
function _bindVerdict(self, fn) {
|
|
2764
|
-
const resolve = self._verdictTail;
|
|
2765
|
-
if (resolve !== null && typeof fn === 'function') {
|
|
2766
|
-
const tail = resolve();
|
|
2767
|
-
if (typeof tail === 'function') fn = _fuseTail(fn, tail);
|
|
2768
|
-
}
|
|
2769
|
-
self.isValidObject = fn;
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
// Bind one of the JSON entry points, through the extension wrapper when there is one.
|
|
2773
|
-
function _bindEntry(self, name, fn) {
|
|
2774
|
-
const ext = self._entryExt;
|
|
2775
|
-
self[name] = ext !== null && ext[name] ? ext[name](fn) : fn;
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
// The JSON entry points under an extension: the schema answers first, and only
|
|
2779
|
-
// text it accepts is parsed for the check, so a rejection costs nothing extra.
|
|
2780
|
-
function _jsonEntryWrappers({ check, errors }) {
|
|
2781
|
-
const parse = (text) => JSON.parse(typeof text === 'string' ? text : new TextDecoder().decode(text));
|
|
2782
|
-
return {
|
|
2783
|
-
validateJSON: (inner) => (text) => {
|
|
2784
|
-
const res = inner(text);
|
|
2785
|
-
if (!res.valid) return res;
|
|
2786
|
-
let data;
|
|
2787
|
-
try { data = parse(text); } catch { return res; }
|
|
2788
|
-
if (check(data)) return res;
|
|
2789
|
-
const e = errors(data);
|
|
2790
|
-
return e && e.length ? { valid: false, errors: e } : { valid: false, errors: [_EXT_FALLBACK] };
|
|
2791
|
-
},
|
|
2792
|
-
isValidJSON: (inner) => (text) => {
|
|
2793
|
-
if (!inner(text)) return false;
|
|
2794
|
-
let data;
|
|
2795
|
-
try { data = parse(text); } catch { return true; }
|
|
2796
|
-
return check(data);
|
|
2797
|
-
},
|
|
2798
|
-
validateAndParse: (inner) => (text) => {
|
|
2799
|
-
const res = inner(text);
|
|
2800
|
-
if (!res.valid) return res;
|
|
2801
|
-
if (check(res.value)) return res;
|
|
2802
|
-
const e = errors(res.value);
|
|
2803
|
-
return { valid: false, value: res.value, errors: e && e.length ? e : [_EXT_FALLBACK] };
|
|
2804
|
-
},
|
|
2805
|
-
};
|
|
2806
|
-
}
|
|
2807
|
-
const _EXT_FALLBACK = Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' });
|
|
2808
|
-
|
|
2809
|
-
// A verdict function that also runs `tail` on what it accepts. The generated
|
|
2810
|
-
// function can take the check in place of its final `return true`, one call
|
|
2811
|
-
// per document; anything else is composed.
|
|
2812
|
-
function _fuseTail(fn, tail) {
|
|
2813
|
-
const fused = typeof fn._withTail === 'function' ? fn._withTail(tail) : null;
|
|
2814
|
-
return fused || ((d) => fn(d) && tail(d));
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
// The rejection validate() returns on the paths where an extension check could
|
|
2818
|
-
// not join the lazy layer: the inner result, plus the check's errors appended
|
|
2819
|
-
// on first read. `inner` may itself be valid, when only the check failed.
|
|
2820
|
-
class ExtendedRejection {
|
|
2821
|
-
constructor(inner, data, collect) {
|
|
2822
|
-
this.valid = false;
|
|
2823
|
-
this._inner = inner;
|
|
2824
|
-
this._data = data;
|
|
2825
|
-
this._collect = collect;
|
|
2826
|
-
this._errors = null;
|
|
2827
|
-
}
|
|
2828
|
-
toJSON() {
|
|
2829
|
-
return { valid: false, errors: this.errors };
|
|
2830
|
-
}
|
|
2831
|
-
_ataRaw() {
|
|
2832
|
-
const inner = this._inner;
|
|
2833
|
-
const more = this._collect(this._data) || [];
|
|
2834
|
-
const raw = inner.valid ? more : (typeof inner._ataRaw === 'function' ? inner._ataRaw() : inner.errors).concat(more);
|
|
2835
|
-
return raw.length ? raw : [_EXT_FALLBACK];
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
Object.defineProperty(ExtendedRejection.prototype, 'errors', {
|
|
2839
|
-
enumerable: true,
|
|
2840
|
-
configurable: true,
|
|
2841
|
-
get() {
|
|
2842
|
-
if (this._errors === null) {
|
|
2843
|
-
const inner = this._inner;
|
|
2844
|
-
const more = this._collect(this._data) || [];
|
|
2845
|
-
const all = inner.valid ? more : inner.errors.concat(more);
|
|
2846
|
-
this._errors = all.length ? all : [_EXT_FALLBACK];
|
|
2847
|
-
}
|
|
2848
|
-
return this._errors;
|
|
2849
|
-
},
|
|
2850
|
-
});
|
|
2851
|
-
|
|
2852
|
-
// For wrappers that enforce a check the schema does not carry, such as the
|
|
2853
|
-
// `instanceof` keyword of @ata-project/keywords. `resolve` is called whenever
|
|
2854
|
-
// the verdict method is bound, which is after the schema has been normalized,
|
|
2855
|
-
// and returns the check, a function of the document that answers true or
|
|
2856
|
-
// false, or null when there is nothing to add. Only isValidObject takes it;
|
|
2857
|
-
// the other entry points, which report errors, stay the wrapper's to handle.
|
|
2858
|
-
//
|
|
2859
|
-
// Before this, such a wrapper had to hold isValidObject behind an accessor so
|
|
2860
|
-
// that the validator's own rebinding could not drop its check, and every call
|
|
2861
|
-
// paid for the accessor and two more calls: 10.1 ns against 4.2 on a document
|
|
2862
|
-
// the schema rejects at its third property.
|
|
2863
|
-
Validator.prototype._verdictTail = null;
|
|
2864
|
-
Validator.prototype._validateTail = null;
|
|
2865
|
-
Validator.prototype._entryExt = null;
|
|
2866
|
-
|
|
2867
|
-
// Let `new Validator(schema)` with the same schema object return this instance.
|
|
2868
|
-
// Only an instance built without options may answer that call: one built with
|
|
2869
|
-
// options (richErrors: false, coerceTypes, formats, ...) would hand its options
|
|
2870
|
-
// to a caller that asked for none, and an extended one would enforce checks the
|
|
2871
|
-
// caller never registered. Both the caller's object and the normalized one are
|
|
2872
|
-
// keys, since a later caller passes the former.
|
|
2873
|
-
function _rememberInstance(self) {
|
|
2874
|
-
if (!self._noOpts || self._verdictTail !== null || self._validateTail !== null) return;
|
|
2875
|
-
const raw = self._rawSchema;
|
|
2876
|
-
if (raw && typeof raw === 'object' && !_identityCache.has(raw)) _identityCache.set(raw, self);
|
|
2877
|
-
const obj = self._schemaObj;
|
|
2878
|
-
if (obj !== raw && obj && typeof obj === 'object' && !_identityCache.has(obj)) _identityCache.set(obj, self);
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
// An extended validator answers differently from a plain one for the same
|
|
2882
|
-
// schema, so it must not be the instance `new Validator(sameSchema)` hands out.
|
|
2883
|
-
function _leaveIdentityCache(self) {
|
|
2884
|
-
self._noOpts = false;
|
|
2885
|
-
// Nothing is registered before the first compile, so there is nothing to
|
|
2886
|
-
// take back.
|
|
2887
|
-
if (!self._initialized && self._jsFn === null) return;
|
|
2888
|
-
const raw = self._rawSchema;
|
|
2889
|
-
if (raw && typeof raw === 'object' && _identityCache.get(raw) === self) _identityCache.delete(raw);
|
|
2890
|
-
// The compiled form is cached too, at the end of the first compile. Read it
|
|
2891
|
-
// only if it is already materialized: reading it otherwise builds it.
|
|
2892
|
-
if (Object.prototype.hasOwnProperty.call(self, '_schemaObj')) {
|
|
2893
|
-
const obj = self._schemaObj;
|
|
2894
|
-
if (obj && typeof obj === 'object' && _identityCache.get(obj) === self) _identityCache.delete(obj);
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
// The same kind of extension for validate(): `resolve` returns { check, errors }
|
|
2899
|
-
// or null, where `errors(data)` lists the check's own errors, or returns null
|
|
2900
|
-
// when there are none. The check's errors come after the schema's, and a value
|
|
2901
|
-
// that fails only the check is rejected with the check's errors alone. Must be
|
|
2902
|
-
// called before validate() is first used, which is when it is compiled; later
|
|
2903
|
-
// calls throw rather than being silently ignored.
|
|
2904
|
-
Validator.prototype._extendValidate = function (resolve) {
|
|
2905
|
-
if (typeof resolve !== 'function') throw new TypeError('_extendValidate expects a function');
|
|
2906
|
-
if (this._initialized) throw new Error('_extendValidate must be called before the validator compiles');
|
|
2907
|
-
_leaveIdentityCache(this);
|
|
2908
|
-
const prev = this._validateTail;
|
|
2909
|
-
this._validateTail = prev === null ? resolve : () => {
|
|
2910
|
-
const a = prev(), b = resolve();
|
|
2911
|
-
if (!a) return b;
|
|
2912
|
-
if (!b) return a;
|
|
2913
|
-
return {
|
|
2914
|
-
check: (d) => a.check(d) && b.check(d),
|
|
2915
|
-
errors: (d) => {
|
|
2916
|
-
const x = a.errors(d), y = b.errors(d);
|
|
2917
|
-
if (!x) return y;
|
|
2918
|
-
if (!y) return x;
|
|
2919
|
-
return x.concat(y);
|
|
2920
|
-
},
|
|
2921
|
-
};
|
|
2922
|
-
};
|
|
2923
|
-
return this;
|
|
2924
|
-
};
|
|
2925
|
-
|
|
2926
|
-
// Both extensions in one call, from one resolver that returns { check, errors }
|
|
2927
|
-
// or null. This is the form @ata-project/keywords uses: registering costs one
|
|
2928
|
-
// closure, where wrapping the five entry points cost a closure per entry point,
|
|
2929
|
-
// the wrappers themselves and an accessor, most of what building a wrapped
|
|
2930
|
-
// validator took.
|
|
2931
|
-
// parse(data): validate, then return a copy holding only what the schema
|
|
2932
|
-
// declares, the way the parse() export of an ahead-of-time module does, with
|
|
2933
|
-
// the same emitter behind both. Building the copy from the schema's own key
|
|
2934
|
-
// list costs less than finding and deleting unknown keys, and leaves the
|
|
2935
|
-
// caller's object alone. Where the key set cannot be proven (a $ref it cannot
|
|
2936
|
-
// inline, patternProperties, an open object) the method declines with an
|
|
2937
|
-
// error instead of guessing, as the module ships no parse() there; so it does
|
|
2938
|
-
// under options that rewrite input, whose answers a copy would not reproduce.
|
|
2939
|
-
Validator.prototype.parse = function (data) {
|
|
2940
|
-
let fn = this._parseFn;
|
|
2941
|
-
if (fn === undefined) {
|
|
2942
|
-
fn = _buildParse(this);
|
|
2943
|
-
Object.defineProperty(this, '_parseFn', { value: fn, writable: true, configurable: true, enumerable: false });
|
|
2944
|
-
}
|
|
2945
|
-
return fn(data);
|
|
2946
|
-
};
|
|
2947
|
-
|
|
2948
|
-
function _buildParse(self) {
|
|
2949
|
-
const decline = (why, instead = 'Use validate() with removeAdditional instead.') => () => {
|
|
2950
|
-
throw new TypeError(`parse() is not available for this validator: ${why}. ${instead}`);
|
|
2951
|
-
};
|
|
2952
|
-
const o = self._options;
|
|
2953
|
-
// Each refusal names its own reason: a caller who hit the combined one could
|
|
2954
|
-
// not tell a coercion option from a keyword package, and read a deliberate
|
|
2955
|
-
// refusal as a failure of valid data.
|
|
2956
|
-
if (o.coerceTypes) return decline('coerceTypes rewrites the input before it is checked');
|
|
2957
|
-
if (o.removeAdditional === 'all') return decline("removeAdditional: 'all' decides the kept keys at check time");
|
|
2958
|
-
if (self._usesKeywords) return decline('custom keywords are in use, and what they accept is not known to the copy');
|
|
2959
|
-
// Checks added to the validator (withKeywords from @ata-project/keywords
|
|
2960
|
-
// adds instanceof and typeof) only narrow what passes; they do not change
|
|
2961
|
-
// which keys the schema declares. The verdict then goes through
|
|
2962
|
-
// isValidObject, which runs them, and a property they check with instanceof
|
|
2963
|
-
// is carried over as it is (see clone-emit).
|
|
2964
|
-
const extended = self._verdictTail !== null || self._validateTail !== null;
|
|
2965
|
-
const { cloneExprFor } = require('./lib/clone-emit');
|
|
2966
|
-
const expr = cloneExprFor(self._schemaObj);
|
|
2967
|
-
if (!expr) return decline('the set of keys to keep cannot be proven from the schema');
|
|
2968
|
-
let copy;
|
|
2969
|
-
try {
|
|
2970
|
-
// eslint-disable-next-line no-new-func
|
|
2971
|
-
copy = new Function('data', 'return ' + expr);
|
|
2972
|
-
} catch {
|
|
2973
|
-
return decline('code generation is not allowed here');
|
|
2974
|
-
}
|
|
2975
|
-
self._ensureCompiled();
|
|
2976
|
-
const verdict = extended ? (d) => self.isValidObject(d) : self._jsFn;
|
|
2977
|
-
if (typeof self._jsFn !== 'function') return decline('the schema has no generated verdict function');
|
|
2978
|
-
return (data) => {
|
|
2979
|
-
if (!verdict(data)) {
|
|
2980
|
-
const e = new Error('validation failed');
|
|
2981
|
-
e.name = 'AtaValidationError';
|
|
2982
|
-
let target = data;
|
|
2983
|
-
if (self._mutatesInput) {
|
|
2984
|
-
try { target = structuredClone(data); } catch { target = data; }
|
|
2985
|
-
}
|
|
2986
|
-
e.errors = self.validate(target).errors;
|
|
2987
|
-
throw e;
|
|
2988
|
-
}
|
|
2989
|
-
return copy(data);
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
Validator.prototype._extendChecks = function (resolve) {
|
|
2994
|
-
if (typeof resolve !== 'function') throw new TypeError('_extendChecks expects a function');
|
|
2995
|
-
this._extendValidate(resolve);
|
|
2996
|
-
return this._extendVerdict(() => {
|
|
2997
|
-
const x = resolve();
|
|
2998
|
-
return x ? x.check : null;
|
|
2999
|
-
});
|
|
3000
|
-
};
|
|
3001
|
-
|
|
3002
|
-
// Whether this instance enforces a check its schema does not carry. The
|
|
3003
|
-
// ahead-of-time emitters build a module from the schema alone, so they refuse
|
|
3004
|
-
// an instance that says yes rather than emit one that accepts too much.
|
|
3005
|
-
// Reading it resolves the registered checks; only an emitter reads it.
|
|
3006
|
-
Object.defineProperty(Validator.prototype, '_externalChecks', {
|
|
3007
|
-
configurable: true,
|
|
3008
|
-
get() {
|
|
3009
|
-
if (this._validateTail !== null && this._validateTail()) return true;
|
|
3010
|
-
if (this._verdictTail !== null && typeof this._verdictTail() === 'function') return true;
|
|
3011
|
-
return false;
|
|
3012
|
-
},
|
|
3013
|
-
});
|
|
3014
|
-
|
|
3015
|
-
Validator.prototype._extendVerdict = function (resolve) {
|
|
3016
|
-
if (typeof resolve !== 'function') throw new TypeError('_extendVerdict expects a function');
|
|
3017
|
-
_leaveIdentityCache(this);
|
|
3018
|
-
const prev = this._verdictTail;
|
|
3019
|
-
this._verdictTail = prev === null ? resolve : () => {
|
|
3020
|
-
const a = prev(), b = resolve();
|
|
3021
|
-
if (typeof a !== 'function') return b;
|
|
3022
|
-
if (typeof b !== 'function') return a;
|
|
3023
|
-
return (d) => a(d) && b(d);
|
|
3024
|
-
};
|
|
3025
|
-
// A method bound before this call was bound without the check. Rebind it.
|
|
3026
|
-
// An unbound one binds through _bindVerdict on its first call.
|
|
3027
|
-
if (Object.prototype.hasOwnProperty.call(this, 'isValidObject')) _bindVerdict(this, this.isValidObject);
|
|
3028
|
-
return this;
|
|
3029
|
-
};
|
|
3030
|
-
|
|
3031
|
-
function _defineLazyMethod(name, maker) {
|
|
3032
|
-
Object.defineProperty(Validator.prototype, name, {
|
|
3033
|
-
configurable: true,
|
|
3034
|
-
get() {
|
|
3035
|
-
const fn = maker(this);
|
|
3036
|
-
Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
|
|
3037
|
-
return fn;
|
|
3038
|
-
},
|
|
3039
|
-
set(fn) {
|
|
3040
|
-
Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
|
|
3041
|
-
},
|
|
3042
|
-
});
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
for (const [name, pick] of [
|
|
3046
|
-
['_schemaObj', (self) => _materializeSchema(self)],
|
|
3047
|
-
['_usesKeywords', (self) => { _materializeSchema(self); return self._usesKeywords; }],
|
|
3048
|
-
['_schemaIsCallers', (self) => { _materializeSchema(self); return self._schemaIsCallers; }],
|
|
3049
|
-
]) {
|
|
3050
|
-
Object.defineProperty(Validator.prototype, name, {
|
|
3051
|
-
configurable: true,
|
|
3052
|
-
get() { return pick(this); },
|
|
3053
|
-
set(v) { Object.defineProperty(this, name, { value: v, writable: true, configurable: true, enumerable: true }); },
|
|
3054
|
-
});
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
_defineLazyMethod('validate', (self) => (data) => {
|
|
3058
|
-
self._ensureCompiled();
|
|
3059
|
-
return self.validate(data);
|
|
3060
|
-
});
|
|
3061
|
-
_defineLazyMethod('isValidObject', (self) => (data) => {
|
|
3062
|
-
// A validator that rewrites its input goes through the full compile, which
|
|
3063
|
-
// binds a verdict method that runs the rewrite first. So does one whose
|
|
3064
|
-
// schema uses a custom keyword: neither the tier-0 plan nor the code
|
|
3065
|
-
// generator knows the keyword, and either would accept what validate()
|
|
3066
|
-
// rejects.
|
|
3067
|
-
if (self._needsPreprocess() || self._usesKeywords) {
|
|
3068
|
-
self._ensureCompiled();
|
|
3069
|
-
return self.isValidObject(data);
|
|
3070
|
-
}
|
|
3071
|
-
// Lazy: classify + build tier 0 plan on first call, not in constructor.
|
|
3072
|
-
const _tier = classify(self._schemaObj);
|
|
3073
|
-
if (_tier.tier === 0) {
|
|
3074
|
-
const _plan = buildTier0Plan(self._schemaObj);
|
|
3075
|
-
let _n = 0;
|
|
3076
|
-
_bindVerdict(self, (d) => {
|
|
3077
|
-
const r = tier0Validate(_plan, d);
|
|
3078
|
-
if (++_n === 2) {
|
|
3079
|
-
try { self._ensureCodegen(); } catch {}
|
|
3080
|
-
}
|
|
3081
|
-
return r;
|
|
3082
|
-
});
|
|
3083
|
-
} else {
|
|
3084
|
-
// `new Function` is a property of the realm, not of the schema: under a
|
|
3085
|
-
// strict CSP or `--disallow-code-generation-from-strings` this throws
|
|
3086
|
-
// rather than declining, and the EvalError reached the caller of a verdict
|
|
3087
|
-
// method. The full compile can answer without code generation, so fall
|
|
3088
|
-
// through to it. The tier-0 branch above already guarded its own call.
|
|
3089
|
-
try { self._ensureCodegen(); } catch { /* no codegen in this realm */ }
|
|
3090
|
-
// Codegen can bail on shapes it cannot represent; the full compile
|
|
3091
|
-
// binds the native path or the unsupported thrower instead of
|
|
3092
|
-
// leaving this stub to re-dispatch to itself.
|
|
3093
|
-
if (!self._jsFn) self._ensureCompiled();
|
|
3094
|
-
}
|
|
3095
|
-
return self.isValidObject(data);
|
|
3096
|
-
});
|
|
3097
|
-
_defineLazyMethod('validateJSON', (self) => (jsonStr) => {
|
|
3098
|
-
self._ensureCompiled();
|
|
3099
|
-
return self.validateJSON(jsonStr);
|
|
3100
|
-
});
|
|
3101
|
-
_defineLazyMethod('isValidJSON', (self) => (jsonStr) => {
|
|
3102
|
-
self._ensureCompiled();
|
|
3103
|
-
return self.isValidJSON(jsonStr);
|
|
3104
|
-
});
|
|
3105
|
-
_defineLazyMethod('validateAndParse', (self) => (jsonStr) => {
|
|
3106
|
-
if (!native) throw new Error('Native addon required for validateAndParse()');
|
|
3107
|
-
self._ensureCompiled();
|
|
3108
|
-
return self.validateAndParse(jsonStr);
|
|
3109
|
-
});
|
|
3110
|
-
_defineLazyMethod('isValid', (self) => (buf) => {
|
|
3111
|
-
if (!native) throw new Error('Native addon required for isValid() — use validate() or isValidObject() instead');
|
|
3112
|
-
self._ensureCompiled();
|
|
3113
|
-
return self.isValid(buf);
|
|
3114
|
-
});
|
|
3115
|
-
_defineLazyMethod('countValid', (self) => (ndjsonBuf) => {
|
|
3116
|
-
if (!native) throw new Error('Native addon required for countValid()');
|
|
3117
|
-
self._ensureCompiled();
|
|
3118
|
-
return self.countValid(ndjsonBuf);
|
|
3119
|
-
});
|
|
3120
|
-
_defineLazyMethod('batchIsValid', (self) => (buffers) => {
|
|
3121
|
-
if (!native) throw new Error('Native addon required for batchIsValid()');
|
|
3122
|
-
self._ensureCompiled();
|
|
3123
|
-
return self.batchIsValid(buffers);
|
|
3124
|
-
});
|
|
3125
|
-
|
|
3126
741
|
module.exports = {
|
|
3127
|
-
Validator,
|
|
3128
|
-
compile,
|
|
3129
|
-
validate,
|
|
3130
|
-
validateAsync,
|
|
3131
|
-
parseAsync,
|
|
3132
|
-
version,
|
|
3133
|
-
createPaddedBuffer,
|
|
3134
|
-
SIMDJSON_PADDING,
|
|
3135
|
-
parseJSON,
|
|
742
|
+
Validator: core.Validator,
|
|
743
|
+
compile: core.compile,
|
|
744
|
+
validate: core.validate,
|
|
745
|
+
validateAsync: core.validateAsync,
|
|
746
|
+
parseAsync: core.parseAsync,
|
|
747
|
+
version: core.version,
|
|
748
|
+
createPaddedBuffer: core.createPaddedBuffer,
|
|
749
|
+
SIMDJSON_PADDING: core.SIMDJSON_PADDING,
|
|
750
|
+
parseJSON: core.parseJSON,
|
|
3136
751
|
toTypeScript,
|
|
3137
|
-
defineSchema,
|
|
752
|
+
defineSchema: core.defineSchema,
|
|
3138
753
|
renderPretty,
|
|
3139
754
|
renderCompact,
|
|
3140
755
|
toOutput,
|