ata-validator 1.34.0 → 1.36.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/build.d.ts +23 -0
- package/build.mjs +4 -0
- package/compiled.d.ts +17 -0
- package/compiled.js +7 -0
- package/compiled.mjs +3 -0
- package/index.browser.mjs +3 -2
- package/index.d.ts +6 -0
- package/index.js +741 -3035
- package/index.mjs +1 -1
- package/index.node.mjs +10 -0
- package/lib/aot-build.js +46 -0
- package/lib/buffer-gate.js +0 -1
- package/lib/compiled.js +96 -0
- package/lib/defaults.js +88 -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 +216 -128
- package/lib/rejections.js +329 -0
- package/lib/schema-order.js +53 -3
- package/lib/validator-core.js +2031 -0
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +1 -2
- package/package.json +24 -11
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
// Native addon: optional. Core validate() uses JS codegen and works without it.
|
|
2
|
+
// Buffer APIs (isValid, countValid, isValidParallel) require native.
|
|
3
|
+
// Loading is delegated to lib/native-load.js so this file stays free of
|
|
4
|
+
// platform probing and `path` (the browser entry must not pull those in).
|
|
5
|
+
// The addon loads on first use: the buffer APIs, a document past the simdjson
|
|
6
|
+
// threshold, version(). Loading it at require cost about 1.9 ms of a 9.7 ms
|
|
7
|
+
// serverless start, for every process that never calls any of those.
|
|
8
|
+
let _nativeMod;
|
|
9
|
+
function getNative() {
|
|
10
|
+
if (_nativeMod === undefined) _nativeMod = require("./native-load")();
|
|
11
|
+
return _nativeMod;
|
|
12
|
+
}
|
|
13
|
+
const { normalizeKeywords, schemaUsesKeywords } = require('./keywords');
|
|
14
|
+
// The code generator (lib/js-compiler and what builds on it: the scanner, the
|
|
15
|
+
// parse() copy, the ahead-of-time emitters) is registered by the entry that
|
|
16
|
+
// wants it, not required here. index.js registers it; lite.js does not, so a
|
|
17
|
+
// bundle built from lite.js never contains it. Until something registers, the
|
|
18
|
+
// core behaves exactly as it does where `new Function` is blocked: every
|
|
19
|
+
// schema runs on the interpreted engine, which passes the same test suite.
|
|
20
|
+
let _codegen = null;
|
|
21
|
+
let compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined;
|
|
22
|
+
function _registerCodegen(engine) {
|
|
23
|
+
_codegen = engine;
|
|
24
|
+
({ compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined } = engine.jsCompiler);
|
|
25
|
+
}
|
|
26
|
+
const { normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
|
|
27
|
+
const { enabledKeywords, stripDisabledKeywords } = require("./vocabularies");
|
|
28
|
+
const { needsNormalization } = require("./schema-scan");
|
|
29
|
+
const { isV1Dialect } = require("./dialect");
|
|
30
|
+
const { classify } = require("./shape-classifier");
|
|
31
|
+
const { buildTier0Plan, tier0Validate } = require("./tier0");
|
|
32
|
+
|
|
33
|
+
// The closure defaults pass. Only a validator without the generated
|
|
34
|
+
// preprocess pass calls it, so it loads on that first call; loading it with
|
|
35
|
+
// the core measured 0.07 ms of every process's start.
|
|
36
|
+
let _defaults = null;
|
|
37
|
+
function buildDefaultsApplier(schema) {
|
|
38
|
+
if (_defaults === null) _defaults = require('./defaults');
|
|
39
|
+
return _defaults.buildDefaultsApplier(schema);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Build a function that coerces property values to match schema types in-place.
|
|
43
|
+
// Handles string→number, string→integer, string→boolean, number→string, boolean→string.
|
|
44
|
+
function buildCoercer(schema) {
|
|
45
|
+
if (typeof schema !== "object" || schema === null) return null;
|
|
46
|
+
const node = buildNodeCoercer(schema, new Set());
|
|
47
|
+
if (node === null) return null;
|
|
48
|
+
return (data) => { if (typeof data === "object" && data !== null) node(data); };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// What to do with a value that sits at `schema`: coerce it (`scalar`) and then
|
|
52
|
+
// visit what is inside it (`inner`). Null when there is nothing.
|
|
53
|
+
function coercionFor(schema, seen) {
|
|
54
|
+
if (!schema || typeof schema !== "object") return null;
|
|
55
|
+
const scalar = schema.type && !Array.isArray(schema.type) ? buildSingleCoercion(schema.type) : null;
|
|
56
|
+
const inner = buildNodeCoercer(schema, seen);
|
|
57
|
+
return scalar || inner ? { scalar, inner } : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The steps for the object or array at `schema`. `seen` stops a schema that
|
|
61
|
+
// contains itself through an inline cycle.
|
|
62
|
+
function buildNodeCoercer(schema, seen) {
|
|
63
|
+
if (seen.has(schema)) return null;
|
|
64
|
+
seen.add(schema);
|
|
65
|
+
const steps = [];
|
|
66
|
+
if (schema.properties) {
|
|
67
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
68
|
+
// Assignment to `__proto__` would replace the prototype; the generated
|
|
69
|
+
// pass skips it for the same reason.
|
|
70
|
+
if (key === "__proto__") continue;
|
|
71
|
+
const c = coercionFor(prop, seen);
|
|
72
|
+
if (!c) continue;
|
|
73
|
+
const { scalar, inner } = c;
|
|
74
|
+
steps.push((o) => {
|
|
75
|
+
if (!(key in o)) return;
|
|
76
|
+
if (scalar) {
|
|
77
|
+
const v = scalar(o[key]);
|
|
78
|
+
if (v !== undefined) o[key] = v;
|
|
79
|
+
}
|
|
80
|
+
if (inner) {
|
|
81
|
+
const x = o[key];
|
|
82
|
+
if (typeof x === "object" && x !== null) inner(x);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
|
|
88
|
+
const c = coercionFor(schema.items, seen);
|
|
89
|
+
if (c) {
|
|
90
|
+
const { scalar, inner } = c;
|
|
91
|
+
steps.push((o) => {
|
|
92
|
+
if (!Array.isArray(o)) return;
|
|
93
|
+
for (let i = 0; i < o.length; i++) {
|
|
94
|
+
if (scalar) {
|
|
95
|
+
const v = scalar(o[i]);
|
|
96
|
+
if (v !== undefined) o[i] = v;
|
|
97
|
+
}
|
|
98
|
+
if (inner) {
|
|
99
|
+
const x = o[i];
|
|
100
|
+
if (typeof x === "object" && x !== null) inner(x);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
seen.delete(schema);
|
|
107
|
+
if (steps.length === 0) return null;
|
|
108
|
+
if (steps.length === 1) return steps[0];
|
|
109
|
+
return (o) => { for (let i = 0; i < steps.length; i++) steps[i](o); };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildSingleCoercion(targetType) {
|
|
113
|
+
switch (targetType) {
|
|
114
|
+
case "number":
|
|
115
|
+
return (v) => {
|
|
116
|
+
if (typeof v === "string") {
|
|
117
|
+
const n = Number(v);
|
|
118
|
+
if (v !== "" && !isNaN(n)) return n;
|
|
119
|
+
}
|
|
120
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
121
|
+
};
|
|
122
|
+
case "integer":
|
|
123
|
+
return (v) => {
|
|
124
|
+
if (typeof v === "string") {
|
|
125
|
+
const n = Number(v);
|
|
126
|
+
if (v !== "" && Number.isInteger(n)) return n;
|
|
127
|
+
}
|
|
128
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
129
|
+
};
|
|
130
|
+
case "string":
|
|
131
|
+
return (v) => {
|
|
132
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
133
|
+
};
|
|
134
|
+
case "boolean":
|
|
135
|
+
return (v) => {
|
|
136
|
+
if (v === "true" || v === "1") return true;
|
|
137
|
+
if (v === "false" || v === "0") return false;
|
|
138
|
+
};
|
|
139
|
+
default:
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build a function that removes properties not defined in schema.properties.
|
|
145
|
+
// Walks nested objects recursively.
|
|
146
|
+
function buildRemover(schema) {
|
|
147
|
+
if (typeof schema !== "object" || schema === null) return null;
|
|
148
|
+
const actions = [];
|
|
149
|
+
collectRemovals(schema, actions);
|
|
150
|
+
if (actions.length === 0) return null;
|
|
151
|
+
return (data) => {
|
|
152
|
+
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collectRemovals(schema, actions, path) {
|
|
157
|
+
if (typeof schema !== "object" || schema === null || !schema.properties)
|
|
158
|
+
return;
|
|
159
|
+
|
|
160
|
+
// If this level has additionalProperties: false, add a removal action
|
|
161
|
+
if (schema.additionalProperties === false) {
|
|
162
|
+
const allowed = new Set(Object.keys(schema.properties));
|
|
163
|
+
if (!path) {
|
|
164
|
+
actions.push((data) => {
|
|
165
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
166
|
+
return;
|
|
167
|
+
const keys = Object.keys(data);
|
|
168
|
+
for (let i = 0; i < keys.length; i++) {
|
|
169
|
+
if (!allowed.has(keys[i])) delete data[keys[i]];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
const parentPath = path;
|
|
174
|
+
actions.push((data) => {
|
|
175
|
+
let target = data;
|
|
176
|
+
for (let j = 0; j < parentPath.length; j++) {
|
|
177
|
+
if (typeof target !== "object" || target === null) return;
|
|
178
|
+
target = target[parentPath[j]];
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
typeof target !== "object" ||
|
|
182
|
+
target === null ||
|
|
183
|
+
Array.isArray(target)
|
|
184
|
+
)
|
|
185
|
+
return;
|
|
186
|
+
const keys = Object.keys(target);
|
|
187
|
+
for (let i = 0; i < keys.length; i++) {
|
|
188
|
+
if (!allowed.has(keys[i])) delete target[keys[i]];
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Always recurse into nested properties (they may have their own additionalProperties: false)
|
|
195
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
196
|
+
if (prop && typeof prop === "object" && prop.properties) {
|
|
197
|
+
collectRemovals(prop, actions, (path || []).concat(key));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Cloudflare Workers, Deno Deploy and pages under a strict Content-Security-
|
|
203
|
+
// Policy refuse `new Function`. Probed once, lazily, because the answer cannot
|
|
204
|
+
// change within a realm and the probe itself is a code generation attempt.
|
|
205
|
+
let _codegenAvailable = null;
|
|
206
|
+
function codegenAvailable() {
|
|
207
|
+
if (_codegen === null) return false;
|
|
208
|
+
if (_codegenAvailable === null) {
|
|
209
|
+
try {
|
|
210
|
+
_codegenAvailable = new Function('return 1')() === 1;
|
|
211
|
+
} catch {
|
|
212
|
+
_codegenAvailable = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return _codegenAvailable;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Schema compilation cache: same schema string -> reuse compiled functions
|
|
219
|
+
const _compileCache = new Map();
|
|
220
|
+
// Generated preprocess passes, keyed like _compileCache plus the options that
|
|
221
|
+
// shape the pass. `null` records that the generator produced none.
|
|
222
|
+
const _preprocessCache = new Map();
|
|
223
|
+
|
|
224
|
+
// Object identity cache: same schema object reference -> reuse entire compiled state
|
|
225
|
+
// Skips JSON.stringify, cache lookup, and all setup. Near-zero cost for repeated schemas.
|
|
226
|
+
const _identityCache = new WeakMap();
|
|
227
|
+
|
|
228
|
+
const SIMDJSON_PADDING = 64;
|
|
229
|
+
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
230
|
+
// How many calls a validator answers through its verdict function before its
|
|
231
|
+
// validate() and validateJSON() compile the single-function hybrid.
|
|
232
|
+
const HYBRID_TIER_CALLS = 64;
|
|
233
|
+
const ABORT_EARLY_RESULT = Object.freeze({
|
|
234
|
+
valid: false,
|
|
235
|
+
errors: Object.freeze([Object.freeze({
|
|
236
|
+
code: 'ATA9000',
|
|
237
|
+
message: 'validation failed',
|
|
238
|
+
keyword: '__abort_early__',
|
|
239
|
+
path: '',
|
|
240
|
+
})]),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// `_CP_LEN_SOURCE`, the safe-regex embed, and the AOT helpers that consume them
|
|
244
|
+
// now live in `lib/aot.js` — keeping this file free of `fs`/`path`/`__dirname`
|
|
245
|
+
// references so a default import never touches disk. The static AOT methods
|
|
246
|
+
// further down lazily require `./lib/aot`, so they pay nothing until a user
|
|
247
|
+
// calls `bundleStandalone`/`bundle`/etc.
|
|
248
|
+
|
|
249
|
+
// Above this size, simdjson On Demand (selective field access) beats JSON.parse
|
|
250
|
+
// (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
const SIMDJSON_THRESHOLD = 8192;
|
|
254
|
+
|
|
255
|
+
// Resolve a JSON Schema path like "#/properties/name/type" to the schema object
|
|
256
|
+
// that *contains* the failing keyword. Used by verbose mode to populate
|
|
257
|
+
// `parentSchema` on validation errors. Returns undefined if the path can't be
|
|
258
|
+
// walked (malformed pointer or missing intermediate node).
|
|
259
|
+
function resolveSchemaByPath(rootSchema, schemaPath) {
|
|
260
|
+
if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
const stripped = schemaPath.slice(1);
|
|
264
|
+
if (!stripped || stripped === '/') return rootSchema;
|
|
265
|
+
const parts = stripped.split('/').filter(Boolean).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
266
|
+
// The last segment is the keyword that failed (e.g. "type"); parentSchema is
|
|
267
|
+
// the schema object that owns that keyword, so walk all but the last segment.
|
|
268
|
+
let target = rootSchema;
|
|
269
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
270
|
+
if (target == null || typeof target !== 'object') return undefined;
|
|
271
|
+
target = target[parts[i]];
|
|
272
|
+
}
|
|
273
|
+
return target;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Rank an error by walking its schemaPath through the schema object: at each
|
|
277
|
+
// level the segment's index among the node's declared keys. Comparing ranks
|
|
278
|
+
// lexicographically orders errors by keyword declaration order, which is the
|
|
279
|
+
// order AJV emits and what schema authors read top to bottom. Segments that
|
|
280
|
+
// cannot be resolved (cross-schema refs, normalized keys) end the walk; the
|
|
281
|
+
// stable sort then keeps such errors in engine emission order.
|
|
282
|
+
// The rank of a `schemaPath` under a given root is fixed: the schema does not
|
|
283
|
+
// change between validations, so neither does the answer. It was recomputed for
|
|
284
|
+
// every error of every failing document, and computing it is not cheap. Two
|
|
285
|
+
// caches, both keyed on things that do not change:
|
|
286
|
+
//
|
|
287
|
+
// rootSchema -> schemaPath -> rank, so a path is walked once ever
|
|
288
|
+
// node -> key -> its index, so the walk stops calling Object.keys and
|
|
289
|
+
// scanning the result for a string
|
|
290
|
+
//
|
|
291
|
+
// A failing route sees the same handful of schemaPaths over and over, which is
|
|
292
|
+
// what makes the first one worth having.
|
|
293
|
+
const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy } = require('./rejections');
|
|
294
|
+
|
|
295
|
+
// Text that does not parse, reported the same way on every path and platform:
|
|
296
|
+
// ATA9001 with the parser's own message. The native addon used to say only
|
|
297
|
+
// "invalid JSON document", and the pure-JS paths used a keyword of their own.
|
|
298
|
+
function _jsonSyntaxRejection(e) {
|
|
299
|
+
return { valid: false, errors: [{ keyword: '__parse__', instancePath: '', schemaPath: '', params: {}, message: 'invalid JSON: ' + e.message }] };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
// Paths repeat across rejections of the same shape, so the parsed segment
|
|
310
|
+
// list is cached per path string. Entries are frozen: the same array is
|
|
311
|
+
// handed to every issue that names the path.
|
|
312
|
+
//
|
|
313
|
+
// The cache is bucketed by length and searched with ===, not keyed in a Map.
|
|
314
|
+
// A path with an array index in it is concatenated afresh by every rejection,
|
|
315
|
+
// and a Map has to hash each such string from scratch before it can look
|
|
316
|
+
// anything up; that hashing was 30% of a Standard Schema rejection with
|
|
317
|
+
// sixteen issues. Comparing against the few paths of the same length costs
|
|
318
|
+
// less. Both levels are bounded so a stream of array indexes cannot grow it
|
|
319
|
+
// without limit: a full bucket stops caching, too many lengths clear it.
|
|
320
|
+
const _pathBuckets = new Map();
|
|
321
|
+
const PATH_BUCKET_MAX = 32;
|
|
322
|
+
const PATH_LENGTHS_MAX = 256;
|
|
323
|
+
function parsePointerPath(path) {
|
|
324
|
+
if (!path) return EMPTY_PATH;
|
|
325
|
+
const n = path.length;
|
|
326
|
+
let bucket = _pathBuckets.get(n);
|
|
327
|
+
if (bucket !== undefined) {
|
|
328
|
+
for (let i = 0; i < bucket.length; i += 2) if (bucket[i] === path) return bucket[i + 1];
|
|
329
|
+
} else {
|
|
330
|
+
if (_pathBuckets.size >= PATH_LENGTHS_MAX) _pathBuckets.clear();
|
|
331
|
+
bucket = [];
|
|
332
|
+
_pathBuckets.set(n, bucket);
|
|
333
|
+
}
|
|
334
|
+
const segs = Object.freeze(parsePointerPathUncached(path));
|
|
335
|
+
if (bucket.length < PATH_BUCKET_MAX * 2) bucket.push(path, segs);
|
|
336
|
+
return segs;
|
|
337
|
+
}
|
|
338
|
+
const EMPTY_PATH = Object.freeze([]);
|
|
339
|
+
|
|
340
|
+
function parsePointerPathUncached(path) {
|
|
341
|
+
// One pass, no intermediate arrays. Per Standard Schema V1 an array index
|
|
342
|
+
// is emitted as a number and an object key as a string; a segment is an
|
|
343
|
+
// index when it is all digits with no leading zero.
|
|
344
|
+
const out = [];
|
|
345
|
+
const n = path.length;
|
|
346
|
+
let start = 1;
|
|
347
|
+
for (let i = 1; i <= n; i++) {
|
|
348
|
+
if (i !== n && path.charCodeAt(i) !== 47) continue;
|
|
349
|
+
if (i > start) {
|
|
350
|
+
let seg = path.slice(start, i);
|
|
351
|
+
if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
352
|
+
const c0 = seg.charCodeAt(0);
|
|
353
|
+
let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
|
|
354
|
+
if (numeric) {
|
|
355
|
+
for (let k = 1; k < seg.length; k++) {
|
|
356
|
+
const c = seg.charCodeAt(k);
|
|
357
|
+
if (c < 48 || c > 57) { numeric = false; break; }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
out.push({ key: numeric ? Number(seg) : seg });
|
|
361
|
+
}
|
|
362
|
+
start = i + 1;
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function createPaddedBuffer(jsonStr) {
|
|
368
|
+
if (typeof Buffer === 'undefined') throw new Error('createPaddedBuffer requires Node.js Buffer');
|
|
369
|
+
const jsonBuf = Buffer.from(jsonStr);
|
|
370
|
+
const padded = Buffer.allocUnsafe(jsonBuf.length + SIMDJSON_PADDING);
|
|
371
|
+
jsonBuf.copy(padded);
|
|
372
|
+
padded.fill(0, jsonBuf.length);
|
|
373
|
+
return { buffer: padded, length: jsonBuf.length };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Deep-clone a value, copying own symbol keys by reference at every level.
|
|
377
|
+
// Arrays and plain objects are cloned recursively; primitives, RegExp,
|
|
378
|
+
// functions, and other non-plain values are returned as-is. Symbol values
|
|
379
|
+
// (e.g. refinement lists, OPTIONAL markers) are owned by the caller's builder
|
|
380
|
+
// and sharing them is correct — they are never mutated by normalization.
|
|
381
|
+
function _deepCloneWithSymbols(v) {
|
|
382
|
+
if (v === null || typeof v !== 'object') return v;
|
|
383
|
+
if (Array.isArray(v)) {
|
|
384
|
+
const a = new Array(v.length);
|
|
385
|
+
for (let i = 0; i < v.length; i++) a[i] = _deepCloneWithSymbols(v[i]);
|
|
386
|
+
return a;
|
|
387
|
+
}
|
|
388
|
+
// Only clone plain objects (skip RegExp, Date, etc.).
|
|
389
|
+
if (Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) return v;
|
|
390
|
+
const out = Object.create(null);
|
|
391
|
+
for (const k of Object.keys(v)) Object.defineProperty(out, k, { value: _deepCloneWithSymbols(v[k]), writable: true, enumerable: true, configurable: true });
|
|
392
|
+
for (const sym of Object.getOwnPropertySymbols(v)) out[sym] = v[sym];
|
|
393
|
+
return Object.setPrototypeOf(out, Object.prototype);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Normalize a caller-provided schema without mutating the original.
|
|
397
|
+
// Clones only when normalization would change the object (draft-07 keys
|
|
398
|
+
// present or nullable fields present). Internal-only — not exported.
|
|
399
|
+
function _normalizeCallerSchema(s, inheritDraft7) {
|
|
400
|
+
const declares = s && typeof s === 'object' && s.$schema !== undefined
|
|
401
|
+
const needsDraft7 = declares
|
|
402
|
+
? (s.$schema === 'http://json-schema.org/draft-07/schema#' || s.$schema === 'http://json-schema.org/draft-07/schema')
|
|
403
|
+
: !!inheritDraft7
|
|
404
|
+
// One walk answers whether there is anything to do. Almost always there is
|
|
405
|
+
// not, and then the serialize, clone, normalize, serialize, compare below is
|
|
406
|
+
// work spent to find that out. The walk over-reports rather than under, so a
|
|
407
|
+
// schema it clears is one no normalizer would have touched;
|
|
408
|
+
// `tests/test_schema_scan.js` holds that direction against the whole suite.
|
|
409
|
+
if (!needsNormalization(s, needsDraft7)) return s
|
|
410
|
+
|
|
411
|
+
const str = JSON.stringify(s)
|
|
412
|
+
const copy = _deepCloneWithSymbols(s)
|
|
413
|
+
if (needsDraft7) normalizeDraft7(copy, true)
|
|
414
|
+
normalizeNullable(copy)
|
|
415
|
+
normalizeExclusiveBounds(copy)
|
|
416
|
+
// Return original when normalization produced no change, copy otherwise.
|
|
417
|
+
// Kept even though the walk has already said there is work, so that a walk
|
|
418
|
+
// which over-reports still returns exactly what it returned before.
|
|
419
|
+
// Change-detection uses JSON content only; symbols do not affect it.
|
|
420
|
+
return JSON.stringify(copy) === str ? s : copy
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// The identity a document is registered under. Draft-07 ignores every
|
|
424
|
+
// keyword sitting next to `$ref`, so normalization drops them, `$id` among
|
|
425
|
+
// them: that is the right reading for evaluation, where the reference
|
|
426
|
+
// resolves against the retrieval URI rather than the declared `$id`. It is
|
|
427
|
+
// the wrong reading for registration, since `$id` is how the caller names
|
|
428
|
+
// the document. So the identity is read from the normalized copy first and
|
|
429
|
+
// from what the caller passed second. A bare-fragment `$id` is a draft-07
|
|
430
|
+
// anchor rather than a document identity, and normalization has already
|
|
431
|
+
// turned it into `$anchor`, so it is not used here.
|
|
432
|
+
function declaredId(original, normalized) {
|
|
433
|
+
const n = normalized && typeof normalized === 'object' ? normalized.$id : undefined
|
|
434
|
+
if (typeof n === 'string' && n !== '') return n
|
|
435
|
+
const o = original && typeof original === 'object' ? original.$id : undefined
|
|
436
|
+
if (typeof o === 'string' && o !== '' && o[0] !== '#') return o
|
|
437
|
+
return undefined
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// `inheritDraft7` is true when the root schema is draft-07: a retrieved
|
|
441
|
+
// document that declares no dialect is read under the root's draft.
|
|
442
|
+
// The map is derived entirely from what the caller passed, so the same
|
|
443
|
+
// `schemas` gives the same map. A server building one validator per route over
|
|
444
|
+
// a shared registry rebuilt it once per route, normalizing and re-reading the
|
|
445
|
+
// `$id` of every registered schema each time. Keyed by the registry object,
|
|
446
|
+
// and by the draft it is read under, since that changes what normalization
|
|
447
|
+
// does to a document which declares no dialect of its own.
|
|
448
|
+
//
|
|
449
|
+
// Validators share the returned map, so anything that mutates one calls
|
|
450
|
+
// `_ownSchemaMap()` first. There are two such places: registering the vendored
|
|
451
|
+
// meta-schemas during compilation, and `addSchema()`.
|
|
452
|
+
const _schemaMapCache = new WeakMap()
|
|
453
|
+
|
|
454
|
+
function buildSchemaMap(schemas, inheritDraft7) {
|
|
455
|
+
if (!schemas) return null
|
|
456
|
+
const byDraft = _schemaMapCache.get(schemas)
|
|
457
|
+
if (byDraft) {
|
|
458
|
+
const hit = byDraft[inheritDraft7 ? 1 : 0]
|
|
459
|
+
if (hit) return hit
|
|
460
|
+
}
|
|
461
|
+
const map = _buildSchemaMap(schemas, inheritDraft7)
|
|
462
|
+
const slot = byDraft || [null, null]
|
|
463
|
+
slot[inheritDraft7 ? 1 : 0] = map
|
|
464
|
+
if (!byDraft) _schemaMapCache.set(schemas, slot)
|
|
465
|
+
return map
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function _buildSchemaMap(schemas, inheritDraft7) {
|
|
469
|
+
const map = new Map()
|
|
470
|
+
if (Array.isArray(schemas)) {
|
|
471
|
+
for (const s of schemas) {
|
|
472
|
+
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
473
|
+
const id = declaredId(s, normalized)
|
|
474
|
+
if (!id) throw new Error('Schema in schemas option must have $id')
|
|
475
|
+
map.set(id, normalized)
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
for (const [key, s] of Object.entries(schemas)) {
|
|
479
|
+
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
480
|
+
// A retrieved document is addressable both by the URI it was registered
|
|
481
|
+
// under and by the $id it declares. Registering only the $id makes
|
|
482
|
+
// references to the retrieval URI unresolvable.
|
|
483
|
+
map.set(key, normalized)
|
|
484
|
+
const id = declaredId(s, normalized)
|
|
485
|
+
if (id && id !== key) map.set(id, normalized)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return map
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// A schema which names a custom meta-schema in `$schema` is written against
|
|
492
|
+
// whatever dialect that meta-schema declares. A keyword from a vocabulary the
|
|
493
|
+
// dialect does not have is not part of the dialect, so it is an unknown
|
|
494
|
+
// keyword and does not apply. Removing it here means every engine sees the
|
|
495
|
+
// same schema and none of them needs to know about vocabularies.
|
|
496
|
+
//
|
|
497
|
+
// Only the root is consulted. A subschema naming its own `$schema` is its own
|
|
498
|
+
// resource under its own dialect, and the walk stops there rather than
|
|
499
|
+
// applying this dialect's answer to it.
|
|
500
|
+
function _applyVocabularies(schemaObj, original, schemaMap) {
|
|
501
|
+
if (!schemaObj || typeof schemaObj !== 'object') return schemaObj
|
|
502
|
+
const declared = schemaObj.$schema
|
|
503
|
+
if (typeof declared !== 'string') return schemaObj
|
|
504
|
+
const enabled = enabledKeywords(schemaMap.get(declared))
|
|
505
|
+
if (!enabled) return schemaObj
|
|
506
|
+
// `original` is the caller's own object when it reached here unchanged, and
|
|
507
|
+
// that one is never mutated.
|
|
508
|
+
const copy = schemaObj === original
|
|
509
|
+
? _deepCloneWithSymbols(schemaObj)
|
|
510
|
+
: schemaObj
|
|
511
|
+
return stripDisabledKeywords(copy, enabled)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Compile-cache key for a root schema plus its external schemas. Must include
|
|
515
|
+
// the external schema CONTENT, not just their $ids: two validators can share a
|
|
516
|
+
// root schema string and the same $id while pointing that $id at different
|
|
517
|
+
// schemas (separate app instances, test suites, multi-tenant). Keying on $id
|
|
518
|
+
// alone reuses the wrong compiled validator and silently mis-validates.
|
|
519
|
+
function compileCacheKey(schemaStr, schemaMap) {
|
|
520
|
+
if (!schemaMap || schemaMap.size === 0) return schemaStr
|
|
521
|
+
const parts = []
|
|
522
|
+
for (const [id, s] of schemaMap) parts.push(id + '=' + JSON.stringify(s))
|
|
523
|
+
parts.sort()
|
|
524
|
+
return schemaStr + '\0' + parts.join('\0')
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Resolve a cross-schema $ref to its target schema for preprocessing purposes.
|
|
528
|
+
// Handles whole-schema refs (`shared#`), relative-id matching, and JSON pointer
|
|
529
|
+
// fragments (`shared#/properties/id`). Returns null for local-only refs or when
|
|
530
|
+
// the target cannot be found. Used only to read `type`/`properties` for
|
|
531
|
+
// coercion/defaults/removeAdditional, never for validation.
|
|
532
|
+
function resolveRefForPreprocess(ref, schemaMap) {
|
|
533
|
+
if (!schemaMap || schemaMap.size === 0 || typeof ref !== 'string') return null
|
|
534
|
+
const hashIdx = ref.indexOf('#')
|
|
535
|
+
const baseId = hashIdx >= 0 ? ref.slice(0, hashIdx) : ref
|
|
536
|
+
const fragment = hashIdx >= 0 ? ref.slice(hashIdx + 1) : ''
|
|
537
|
+
if (!baseId) return null
|
|
538
|
+
let base = null
|
|
539
|
+
if (schemaMap.has(baseId)) base = schemaMap.get(baseId)
|
|
540
|
+
else if (!ref.includes('://')) {
|
|
541
|
+
for (const [id, s] of schemaMap) {
|
|
542
|
+
if (id.endsWith('/' + baseId)) { base = s; break }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (!base) return null
|
|
546
|
+
if (!fragment) return base
|
|
547
|
+
let target = base
|
|
548
|
+
for (const part of fragment.split('/')) {
|
|
549
|
+
if (part === '') continue
|
|
550
|
+
if (target == null || typeof target !== 'object') return null
|
|
551
|
+
target = target[part.replace(/~1/g, '/').replace(/~0/g, '~')]
|
|
552
|
+
}
|
|
553
|
+
return target == null ? null : target
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Preprocessing (coerce/defaults/removeAdditional) reads `schema.properties` and
|
|
557
|
+
// each property's `type`. When the data shape lives behind a cross-schema $ref
|
|
558
|
+
// (a whole-schema ref like Fastify's `params: { $ref: 'shared#' }`, or a
|
|
559
|
+
// property ref like `{ id: { $ref: 'shared#/properties/id' } }`), follow the
|
|
560
|
+
// ref so the preprocessor can see the referenced shape. Returns the schema with
|
|
561
|
+
// such refs resolved, cloning only when a substitution is made.
|
|
562
|
+
function resolveSchemaForPreprocess(schema, schemaMap) {
|
|
563
|
+
if (!schema || typeof schema !== 'object' || !schemaMap || schemaMap.size === 0) return schema
|
|
564
|
+
let s = schema
|
|
565
|
+
// Whole-schema ref (only when it has no own properties, to avoid dropping
|
|
566
|
+
// sibling keywords on schemas that mix $ref with properties).
|
|
567
|
+
if (s.$ref && !s.properties) {
|
|
568
|
+
const t = resolveRefForPreprocess(s.$ref, schemaMap)
|
|
569
|
+
if (t && typeof t === 'object') s = t
|
|
570
|
+
}
|
|
571
|
+
if (!s.properties) return s
|
|
572
|
+
// Property-level refs: substitute the resolved target so coercion sees `type`.
|
|
573
|
+
let cloned = null
|
|
574
|
+
for (const key of Object.keys(s.properties)) {
|
|
575
|
+
const p = s.properties[key]
|
|
576
|
+
if (p && typeof p === 'object' && p.$ref && !p.type) {
|
|
577
|
+
const t = resolveRefForPreprocess(p.$ref, schemaMap)
|
|
578
|
+
if (t && typeof t === 'object') {
|
|
579
|
+
if (!cloned) { cloned = Object.assign({}, s); cloned.properties = Object.assign({}, s.properties) }
|
|
580
|
+
cloned.properties[key] = t
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return cloned || s
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// `_schemaObj` and `_usesKeywords` are materialized together on first read:
|
|
588
|
+
// the caller's schema normalized on a clone (the caller's object is never
|
|
589
|
+
// touched), `format` stripped under `assertFormat: false`, and the custom
|
|
590
|
+
// keyword scan. The accessors then step aside for own data properties, so
|
|
591
|
+
// every later read is a plain field.
|
|
592
|
+
function _materializeSchema(self) {
|
|
593
|
+
const raw = self._rawSchema;
|
|
594
|
+
const options = self._options;
|
|
595
|
+
let schemaObj = _normalizeCallerSchema(raw);
|
|
596
|
+
// `baseURI` is where the schema was retrieved from, the base its relative
|
|
597
|
+
// references resolve against when it declares no `$id` of its own. Draft-07
|
|
598
|
+
// ignores an `$id` beside a root `$ref`, so SchemaStore's schemas, which are
|
|
599
|
+
// written that way, lost their base and every relative reference in them
|
|
600
|
+
// failed to resolve; an editor always knows the address it downloaded a
|
|
601
|
+
// schema from. Set on a shallow copy, never on the caller's object.
|
|
602
|
+
if (typeof options.baseURI === 'string' && options.baseURI !== '' &&
|
|
603
|
+
schemaObj && typeof schemaObj === 'object' && !Array.isArray(schemaObj) && typeof schemaObj.$id !== 'string') {
|
|
604
|
+
schemaObj = { ...schemaObj, $id: options.baseURI };
|
|
605
|
+
}
|
|
606
|
+
const isCallers = self._rawIsCallers && schemaObj === raw;
|
|
607
|
+
if (options.assertFormat === false) {
|
|
608
|
+
schemaObj = stripFormatAssertions(isCallers ? _deepCloneWithSymbols(schemaObj) : schemaObj);
|
|
609
|
+
}
|
|
610
|
+
const usesKeywords = self._keywords !== null && schemaUsesKeywords(schemaObj, self._keywords);
|
|
611
|
+
Object.defineProperty(self, '_schemaObj', { value: schemaObj, writable: true, configurable: true, enumerable: true });
|
|
612
|
+
Object.defineProperty(self, '_usesKeywords', { value: usesKeywords, writable: true, configurable: true, enumerable: true });
|
|
613
|
+
Object.defineProperty(self, '_schemaIsCallers', { value: isCallers && schemaObj === raw, writable: true, configurable: true, enumerable: true });
|
|
614
|
+
return schemaObj;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
class Validator {
|
|
618
|
+
constructor(schema, opts) {
|
|
619
|
+
const options = opts || {};
|
|
620
|
+
|
|
621
|
+
// Ultra-fast path: same schema object reference -> return cached instance
|
|
622
|
+
// JS constructor returning an object makes `new` return that object
|
|
623
|
+
// Cost: one WeakMap lookup. No property copy, no setup, nothing.
|
|
624
|
+
if (!opts && typeof schema === "object" && schema !== null) {
|
|
625
|
+
const hit = _identityCache.get(schema);
|
|
626
|
+
if (hit) return hit;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// The schema is not walked here. Normalization (draft-07 rewrites,
|
|
630
|
+
// nullable, `assertFormat: false`) and the scan that decides whether any
|
|
631
|
+
// of it is needed run on the first read of `_schemaObj`, which is the
|
|
632
|
+
// first compile. Construction is the object and its fields; a server
|
|
633
|
+
// building a validator per request pays nothing for a schema it never
|
|
634
|
+
// uses, and a benchmark timing construction measures construction.
|
|
635
|
+
// When schema is a string, JSON.parse already produces a fresh object.
|
|
636
|
+
const raw = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
637
|
+
const rootIsDraft7 = !!(raw && typeof raw === 'object' && typeof raw.$schema === 'string' &&
|
|
638
|
+
(raw.$schema === 'http://json-schema.org/draft-07/schema#' || raw.$schema === 'http://json-schema.org/draft-07/schema'));
|
|
639
|
+
|
|
640
|
+
// Built here rather than below because `$vocabulary` is resolved against
|
|
641
|
+
// it, and that resolution waits until compilation so a meta-schema
|
|
642
|
+
// registered by addSchema() still counts.
|
|
643
|
+
const shared = buildSchemaMap(options.schemas, rootIsDraft7);
|
|
644
|
+
const schemaMap = shared || new Map();
|
|
645
|
+
this._schemaMapShared = shared !== null;
|
|
646
|
+
this._vocabulariesApplied = false;
|
|
647
|
+
|
|
648
|
+
// Custom keywords, normalized once. `_usesKeywords` (resolved with the
|
|
649
|
+
// schema) is what routes the schema to the interpreted engine and keeps
|
|
650
|
+
// it out of the shared compile cache; a schema that registers keywords
|
|
651
|
+
// but uses none of them takes the ordinary path.
|
|
652
|
+
this._keywords = normalizeKeywords(options.keywords);
|
|
653
|
+
|
|
654
|
+
this._schemaStr = null; // lazy: computed on first use
|
|
655
|
+
this._rawSchema = raw;
|
|
656
|
+
this._rawIsCallers = typeof schema !== "string";
|
|
657
|
+
this._options = options;
|
|
658
|
+
this._noOpts = !opts;
|
|
659
|
+
// engine: 'interpreter' keeps this validator off code generation: no
|
|
660
|
+
// `new Function`, no shared compile cache, the eval-free interpreted
|
|
661
|
+
// engine answers validate(), isValidObject() and validateJSON(). For a
|
|
662
|
+
// schema that arrives from outside the trust boundary (a plugin's
|
|
663
|
+
// declared config shape, a tenant's upload), where turning it into source
|
|
664
|
+
// is not an acceptable execution model. ATA_FORCE_NAPI does this for the
|
|
665
|
+
// whole process; the option does it for one validator. A misspelling
|
|
666
|
+
// must not fall through to codegen, so anything else is refused.
|
|
667
|
+
if (options.engine !== undefined && options.engine !== 'auto' && options.engine !== 'interpreter') {
|
|
668
|
+
throw new TypeError("engine must be 'auto' or 'interpreter', got " + JSON.stringify(options.engine));
|
|
669
|
+
}
|
|
670
|
+
this._interpretOnly = options.engine === 'interpreter';
|
|
671
|
+
this._initialized = false;
|
|
672
|
+
this._nativeReady = false;
|
|
673
|
+
this._compiled = null;
|
|
674
|
+
this._fastSlot = -1;
|
|
675
|
+
this._jsFn = null;
|
|
676
|
+
this._engine = undefined;
|
|
677
|
+
this._preprocess = null;
|
|
678
|
+
this._applyDefaults = null;
|
|
679
|
+
|
|
680
|
+
// Schema map for cross-schema $ref resolution
|
|
681
|
+
this._schemaMap = schemaMap;
|
|
682
|
+
|
|
683
|
+
// User-supplied format checkers: { formatName: (value) => boolean }.
|
|
684
|
+
// Looked up at runtime when a schema references a format the built-in
|
|
685
|
+
// registry does not know about.
|
|
686
|
+
this._userFormats = options.formats || null;
|
|
687
|
+
|
|
688
|
+
// Verbose mode: when on, errors carry parentSchema (the schema object that
|
|
689
|
+
// produced the error). Matches ajv's `verbose: true` behavior.
|
|
690
|
+
this._verbose = !!options.verbose;
|
|
691
|
+
|
|
692
|
+
// strictSchema: authoring-time checks, off by default. A mistyped keyword
|
|
693
|
+
// is the one schema mistake that fails open: to every dialect `maxLenght`
|
|
694
|
+
// is an annotation, so the constraint the author meant is simply absent
|
|
695
|
+
// and previously invalid data validates. `true` throws here, at
|
|
696
|
+
// construction, with every finding; `'log'` reports through
|
|
697
|
+
// `options.logger` or the console and continues. The check runs on the
|
|
698
|
+
// schema as written, before any normalization touches it.
|
|
699
|
+
if (options.strictSchema === true || options.strictSchema === 'log') {
|
|
700
|
+
const { checkSchemaStrict } = require('./strict-check');
|
|
701
|
+
const problems = checkSchemaStrict(schema, { userKeywords: options.keywords || null });
|
|
702
|
+
if (problems.length > 0) {
|
|
703
|
+
const text = problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n');
|
|
704
|
+
if (options.strictSchema === true) {
|
|
705
|
+
throw new Error(text);
|
|
706
|
+
}
|
|
707
|
+
const logger = options.logger;
|
|
708
|
+
if (logger !== false) {
|
|
709
|
+
const warn = logger && typeof logger.warn === 'function' ? logger.warn.bind(logger) : console.warn;
|
|
710
|
+
warn(text);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// richErrors: default true. Only the literal `false` opts back into the
|
|
716
|
+
// v0.14 error shape (no code/expected/received/docUrl, no aliases).
|
|
717
|
+
this._richErrors = options && options.richErrors === false ? false : true;
|
|
718
|
+
|
|
719
|
+
// Optional schema source descriptor. When supplied, the renderer pipeline
|
|
720
|
+
// can attach a `schemaSource` frame to enriched errors.
|
|
721
|
+
this._source = options && options.source && typeof options.source === 'object'
|
|
722
|
+
? { path: String(options.source.path || ''), content: String(options.source.content || '') }
|
|
723
|
+
: null;
|
|
724
|
+
|
|
725
|
+
// Build a JSON pointer -> position map for the schema text once at
|
|
726
|
+
// construction so each runtime error can resolve `schemaSource` without
|
|
727
|
+
// re-scanning the source on every validate() call.
|
|
728
|
+
if (this._source) {
|
|
729
|
+
const { buildPositionMap } = require('./source-positions');
|
|
730
|
+
this._schemaPositions = buildPositionMap(this._source.content);
|
|
731
|
+
} else {
|
|
732
|
+
this._schemaPositions = null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Per-validate data position cache. Populated by validateJSON before
|
|
736
|
+
// dispatching to inner validate(); consulted by the rich-error wrap
|
|
737
|
+
// to attach dataFrame entries to each enriched error.
|
|
738
|
+
this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
|
|
739
|
+
this._lastRawInput = null;
|
|
740
|
+
// undefined: not built yet. null: this schema has no scanner.
|
|
741
|
+
this._scanner = undefined;
|
|
742
|
+
// Checks a wrapper registered through _extendChecks. Declared here so
|
|
743
|
+
// registering one does not change the instance's shape.
|
|
744
|
+
this._verdictTail = null;
|
|
745
|
+
this._validateTail = null;
|
|
746
|
+
this._entryExt = null;
|
|
747
|
+
|
|
748
|
+
// Public methods start as memoized accessors on the prototype; nothing is
|
|
749
|
+
// allocated per instance until one is first read. See _defineLazyMethod
|
|
750
|
+
// below the class.
|
|
751
|
+
|
|
752
|
+
// "~standard" (Standard Schema V1) is a lazy prototype accessor too;
|
|
753
|
+
// see below the class. Consumers only pay for it if they read it.
|
|
754
|
+
|
|
755
|
+
// The identity cache, which lets a later `new Validator(sameSchema)` return
|
|
756
|
+
// this instance, is filled on the first compile rather than here. A WeakMap
|
|
757
|
+
// entry is an ephemeron the collector has to trace separately, and setting
|
|
758
|
+
// one cost about 780 ns against 150 for the rest of this constructor, five
|
|
759
|
+
// times over for an instance that may never validate anything. Once an
|
|
760
|
+
// instance has compiled, the shortcut behaves as before.
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// `$vocabulary` says which keywords the dialect has, and answering needs the
|
|
764
|
+
// meta-schema, which addSchema() may only have registered just now. Run once,
|
|
765
|
+
// before anything reads the schema, and before `_schemaStr` is computed from
|
|
766
|
+
// it. After this addSchema() is refused, so the answer cannot go stale.
|
|
767
|
+
// Whether validation is preceded by a pass that rewrites the input:
|
|
768
|
+
// coercion, removal of undeclared keys, or filling in defaults. The verdict
|
|
769
|
+
// methods have to take the same path when it is, so the quick bindings that
|
|
770
|
+
// answer from the compiled function alone are not used for these validators.
|
|
771
|
+
_needsPreprocess() {
|
|
772
|
+
const o = this._options;
|
|
773
|
+
if (o.coerceTypes || o.removeAdditional) return true;
|
|
774
|
+
if (o.useDefaults === false) return false;
|
|
775
|
+
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
776
|
+
return this._schemaStr.includes('"default"');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
_pos() {
|
|
780
|
+
return this._posCache || (this._posCache = require('./data-position-cache').createCache());
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
_ensureVocabularies() {
|
|
784
|
+
if (this._vocabulariesApplied) return;
|
|
785
|
+
this._vocabulariesApplied = true;
|
|
786
|
+
const stripped = _applyVocabularies(
|
|
787
|
+
this._schemaObj,
|
|
788
|
+
this._schemaIsCallers ? this._schemaObj : null,
|
|
789
|
+
this._schemaMap,
|
|
790
|
+
);
|
|
791
|
+
if (stripped !== this._schemaObj) {
|
|
792
|
+
this._schemaObj = stripped;
|
|
793
|
+
this._schemaStr = null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
_ensureCompiled() {
|
|
798
|
+
if (this._initialized) return;
|
|
799
|
+
this._ensureVocabularies();
|
|
800
|
+
this._initialized = true;
|
|
801
|
+
|
|
802
|
+
const schemaObj = this._schemaObj;
|
|
803
|
+
const options = this._options;
|
|
804
|
+
|
|
805
|
+
// Lazy stringify — only computed here, not in constructor
|
|
806
|
+
if (!this._schemaStr) this._schemaStr = JSON.stringify(schemaObj);
|
|
807
|
+
|
|
808
|
+
// A $ref to a meta-schema resolves from the vendored copies, so
|
|
809
|
+
// "validate this schema against its dialect" needs no network and no
|
|
810
|
+
// caller-supplied registry. Only schemas that mention json-schema.org in a
|
|
811
|
+
// reference pay for the lookup.
|
|
812
|
+
if (this._schemaStr.includes('json-schema.org/draft')) {
|
|
813
|
+
const { METASCHEMAS } = require('./metaschemas');
|
|
814
|
+
this._ownSchemaMap();
|
|
815
|
+
for (const [id, meta] of METASCHEMAS) {
|
|
816
|
+
const bare = id.replace(/#$/, '');
|
|
817
|
+
for (const key of [id, bare, bare + '#', bare.replace(/^https:/, 'http:'), bare.replace(/^http:/, 'https:')]) {
|
|
818
|
+
if (!this._schemaMap.has(key)) this._schemaMap.set(key, meta);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Check cache first -- reuse compiled functions for same schema
|
|
824
|
+
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
825
|
+
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
826
|
+
var _forceNapi = this._interpretOnly || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI);
|
|
827
|
+
// Custom formats are JS functions: bypass the compile cache since they can
|
|
828
|
+
// differ between validators that share the same schema string. An
|
|
829
|
+
// interpreter-only validator never touches it either way: a function a
|
|
830
|
+
// trusted validator compiled for the same schema string must not answer
|
|
831
|
+
// for it.
|
|
832
|
+
const cached = (this._userFormats || this._usesKeywords || _forceNapi) ? null : _compileCache.get(mapKey);
|
|
833
|
+
let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
|
|
834
|
+
// v1 removes the bookending requirement for $dynamicRef. Only the
|
|
835
|
+
// interpreted engine implements that; the JS compiler and the native
|
|
836
|
+
// engine both resolve the 2020-12 way, so a v1 schema using the keyword
|
|
837
|
+
// goes to the interpreter rather than being validated under the wrong
|
|
838
|
+
// dialect. Schemas without $dynamicRef are unaffected: v1 and 2020-12
|
|
839
|
+
// agree on everything else ata implements.
|
|
840
|
+
this._v1Dynamic =
|
|
841
|
+
isV1Dialect(schemaObj) &&
|
|
842
|
+
(this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"'));
|
|
843
|
+
//
|
|
844
|
+
// Where source cannot be turned into a function, neither JS path is usable
|
|
845
|
+
// either. The closure path does not call `new Function` itself, so it
|
|
846
|
+
// survives the block and would quietly handle schemas it gets wrong; the
|
|
847
|
+
// interpreted engine is both eval-free and more correct, so go straight
|
|
848
|
+
// there. The forced case is decided before the probe: the probe is a
|
|
849
|
+
// `new Function` too, and a validator that promised none must not run it.
|
|
850
|
+
if (_forceNapi || this._v1Dynamic || this._usesKeywords || !codegenAvailable()) {
|
|
851
|
+
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
852
|
+
// `full` separates an entry that holds every compiled function from one
|
|
853
|
+
// the verdict-only fast path seeded, where `combined` and `errFn` are null
|
|
854
|
+
// because nothing has tried to build them yet. Both halves of that
|
|
855
|
+
// distinction are null, and reading the second as the first costs this
|
|
856
|
+
// schema its generated error function for the life of the process.
|
|
857
|
+
} else if (cached && cached.jsFn !== undefined) {
|
|
858
|
+
// `full` says the error and combined functions exist too. An entry
|
|
859
|
+
// without it still carries a verdict function worth reusing; the pair is
|
|
860
|
+
// built by _buildErr/_buildCombined below when something asks, and the
|
|
861
|
+
// entry is upgraded then. `undefined` in `combined`/`errFn` means not
|
|
862
|
+
// built yet; `null` means the compiler declined. Those two must never
|
|
863
|
+
// blur: reading the first as the second is the bug this cache had once
|
|
864
|
+
// already, and it silently cost schemas their generated error function.
|
|
865
|
+
jsFn = cached.jsFn;
|
|
866
|
+
jsCombinedFn = cached.combined;
|
|
867
|
+
jsErrFn = cached.errFn;
|
|
868
|
+
_isCodegen = !!cached.isCodegen;
|
|
869
|
+
this._engine = _isCodegen ? 'codegen' : jsFn ? 'closure' : null;
|
|
870
|
+
} else {
|
|
871
|
+
const uf = this._userFormats;
|
|
872
|
+
const _cgFn = compileToJSCodegen(schemaObj, sm, uf);
|
|
873
|
+
jsFn = _cgFn || compileToJS(schemaObj, null, sm);
|
|
874
|
+
// Only the verdict is compiled here. The error and combined generators
|
|
875
|
+
// are the other two thirds of a cold first call (8.2, 10.4 and 7.8 ms on
|
|
876
|
+
// a 120-property config schema, most of it V8 compiling each generator
|
|
877
|
+
// the first time it is entered), and a caller that never reads an error
|
|
878
|
+
// never needs them. _buildErr/_buildCombined compile them on demand.
|
|
879
|
+
jsCombinedFn = undefined;
|
|
880
|
+
jsErrFn = undefined;
|
|
881
|
+
_isCodegen = !!_cgFn;
|
|
882
|
+
this._engine = _cgFn ? 'codegen' : jsFn ? 'closure' : null;
|
|
883
|
+
if (!uf) {
|
|
884
|
+
_compileCache.set(mapKey, { jsFn, combined: undefined, errFn: undefined, isCodegen: _isCodegen, full: false });
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
this._jsFn = jsFn;
|
|
888
|
+
if (this._engine === undefined) this._engine = null;
|
|
889
|
+
|
|
890
|
+
// Data mutators -- try codegen first (12x faster), fallback to closure arrays.
|
|
891
|
+
// Follow cross-refs so coercion/defaults/removeAdditional see the referenced
|
|
892
|
+
// shape (e.g. Fastify `params: { $ref: 'shared#' }` or property refs like
|
|
893
|
+
// `{ id: { $ref: 'shared#/properties/id' } }`).
|
|
894
|
+
const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
|
|
895
|
+
// The mutator pass (defaults, coercion, removal) is generated source too,
|
|
896
|
+
// with the schema's `default` values embedded; an interpreter-only
|
|
897
|
+
// validator takes the closure mutators instead. The generated pass is a
|
|
898
|
+
// function of the schema text, the referenced schemas and three options,
|
|
899
|
+
// and holds no state, so validators that share those share it, as they
|
|
900
|
+
// share the verdict function: a Fastify app repeats a body schema across
|
|
901
|
+
// routes, and generating and compiling the pass again for each was most of
|
|
902
|
+
// what a repeated schema cost to set up.
|
|
903
|
+
let preprocess = null;
|
|
904
|
+
if (!this._interpretOnly && _codegen !== null) {
|
|
905
|
+
const cacheable = !this._userFormats && !this._usesKeywords;
|
|
906
|
+
const ppKey = cacheable ? mapKey + '\u0000' + String(options.coerceTypes) + '|' + (options.useDefaults !== false) + '|' + String(options.removeAdditional) : null;
|
|
907
|
+
const hit = ppKey !== null ? _preprocessCache.get(ppKey) : undefined;
|
|
908
|
+
if (hit !== undefined) preprocess = hit;
|
|
909
|
+
else {
|
|
910
|
+
preprocess = _codegen.buildPreprocess(preprocessSchema, options) || null;
|
|
911
|
+
if (ppKey !== null) _preprocessCache.set(ppKey, preprocess);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (!preprocess) {
|
|
915
|
+
// A root schema whose text has no `default` has none to fill, and the
|
|
916
|
+
// defaults module then stays unloaded. One reached through a reference
|
|
917
|
+
// into another document is walked, since its defaults are not in this
|
|
918
|
+
// text.
|
|
919
|
+
const mayHaveDefaults = preprocessSchema !== schemaObj || this._schemaStr.includes('"default"');
|
|
920
|
+
const applyDefaults = options.useDefaults === false || !mayHaveDefaults ? null : buildDefaultsApplier(preprocessSchema);
|
|
921
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
|
|
922
|
+
const applyRemove = options.removeAdditional
|
|
923
|
+
? buildRemover(preprocessSchema)
|
|
924
|
+
: null;
|
|
925
|
+
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
926
|
+
preprocess =
|
|
927
|
+
mutators.length === 0
|
|
928
|
+
? null
|
|
929
|
+
: mutators.length === 1
|
|
930
|
+
? mutators[0]
|
|
931
|
+
: (data) => {
|
|
932
|
+
for (let i = 0; i < mutators.length; i++) mutators[i](data);
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
this._applyDefaults = preprocess;
|
|
936
|
+
// Whether validate() can change the caller's object before the verdict.
|
|
937
|
+
// This is a capability, not an option: `useDefaults` is on by default, but
|
|
938
|
+
// buildDefaultsApplier returns null when the schema declares no defaults,
|
|
939
|
+
// so a plain schema is genuinely non-mutating. The renderers refuse to
|
|
940
|
+
// synthesize a frame when this is true, because a frame built from mutated
|
|
941
|
+
// data would show the reader a value they never sent.
|
|
942
|
+
this._mutatesInput = !!(preprocess || options.coerceTypes || options.removeAdditional);
|
|
943
|
+
this._preprocess = preprocess;
|
|
944
|
+
|
|
945
|
+
// removeAdditional alone, the common parse-and-strip use: a verdict function
|
|
946
|
+
// that deletes unknown keys in the walk it already makes, where the pass
|
|
947
|
+
// above walks every object a second time just to find them. It answers
|
|
948
|
+
// the documents it accepts; anything it rejects takes the full path below,
|
|
949
|
+
// which removes, validates and reports exactly as before, so a rejected
|
|
950
|
+
// document is left as clean as it always was. The generator declines any
|
|
951
|
+
// schema where deleting during the walk could change an answer.
|
|
952
|
+
let fusedRemove = null;
|
|
953
|
+
if (preprocess && options.removeAdditional && !options.coerceTypes && !this._interpretOnly &&
|
|
954
|
+
!this._userFormats && !this._usesKeywords && !this._schemaStr.includes('"default"')) {
|
|
955
|
+
try {
|
|
956
|
+
fusedRemove = compileToJSCodegen(schemaObj, this._schemaMap.size > 0 ? this._schemaMap : null, null, { removeAdditional: true });
|
|
957
|
+
} catch {
|
|
958
|
+
fusedRemove = null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Detect if schema is "selective" -- doesn't recurse into arrays/deep objects.
|
|
963
|
+
const hasArrayTraversal =
|
|
964
|
+
schemaObj &&
|
|
965
|
+
(schemaObj.items ||
|
|
966
|
+
schemaObj.prefixItems ||
|
|
967
|
+
schemaObj.contains ||
|
|
968
|
+
(schemaObj.properties &&
|
|
969
|
+
Object.values(schemaObj.properties).some(
|
|
970
|
+
(p) => p && (p.items || p.prefixItems || p.contains),
|
|
971
|
+
)));
|
|
972
|
+
const useSimdjsonForLarge = !hasArrayTraversal;
|
|
973
|
+
|
|
974
|
+
// Build the generators the compile step left out, each only when something
|
|
975
|
+
// asks for it, and upgrade the shared cache entry. A first rejection needs
|
|
976
|
+
// one of the two, not both; building both was a millisecond of V8 compiling
|
|
977
|
+
// a generator nobody called. `undefined` means not built yet; `null` means
|
|
978
|
+
// the compiler declined. Conflating those is what once cost every schema
|
|
979
|
+
// its generated error function for the life of the process, so they stay
|
|
980
|
+
// apart, and `full` is set only once both exist.
|
|
981
|
+
const _upgradeCacheEntry = () => {
|
|
982
|
+
if (this._userFormats) return;
|
|
983
|
+
const entry = _compileCache.get(mapKey);
|
|
984
|
+
if (entry && entry.jsFn === jsFn) {
|
|
985
|
+
if (jsCombinedFn !== undefined) entry.combined = jsCombinedFn;
|
|
986
|
+
if (jsErrFn !== undefined) entry.errFn = jsErrFn;
|
|
987
|
+
if (jsCombinedFn !== undefined && jsErrFn !== undefined) entry.full = true;
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
const _buildCombined = () => {
|
|
991
|
+
if (jsCombinedFn !== undefined) return;
|
|
992
|
+
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, this._userFormats) || null;
|
|
993
|
+
_upgradeCacheEntry();
|
|
994
|
+
};
|
|
995
|
+
const _buildErr = () => {
|
|
996
|
+
if (jsErrFn !== undefined) return;
|
|
997
|
+
jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, this._userFormats) || null;
|
|
998
|
+
_upgradeCacheEntry();
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
if (jsFn) {
|
|
1002
|
+
_codegen.installPaths.call(this, {
|
|
1003
|
+
jsFn, _isCodegen, preprocess, fusedRemove, options, schemaObj, useSimdjsonForLarge, _buildCombined, _buildErr,
|
|
1004
|
+
combined: () => jsCombinedFn, err: () => jsErrFn,
|
|
1005
|
+
});
|
|
1006
|
+
} else {
|
|
1007
|
+
// No JS codegen: the interpreted engine, with or without the addon. The
|
|
1008
|
+
// buffer APIs come from _installBufferApis on first use, as they do on
|
|
1009
|
+
// every other path.
|
|
1010
|
+
const { createInterpreter } = require('./interpreter');
|
|
1011
|
+
const interp = createInterpreter(schemaObj, {
|
|
1012
|
+
schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
|
|
1013
|
+
formats: this._userFormats,
|
|
1014
|
+
v1: isV1Dialect(schemaObj),
|
|
1015
|
+
keywords: this._keywords,
|
|
1016
|
+
});
|
|
1017
|
+
this._engine = 'interpreter';
|
|
1018
|
+
if (!preprocess) this._fastVerdict = (d) => interp.isValid(d);
|
|
1019
|
+
// abortEarly is a documented contract, not a property of whichever engine
|
|
1020
|
+
// answered: it promises the frozen ATA9000 stub instead of a detailed
|
|
1021
|
+
// error, so code that branches on it has to behave the same with and
|
|
1022
|
+
// without code generation. Taking the verdict path here also skips
|
|
1023
|
+
// building the errors the caller said it did not want.
|
|
1024
|
+
const run = options.abortEarly
|
|
1025
|
+
? (preprocess
|
|
1026
|
+
? (data) => { preprocess(data); return interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT; }
|
|
1027
|
+
: (data) => (interp.isValid(data) ? VALID_RESULT : ABORT_EARLY_RESULT))
|
|
1028
|
+
: (preprocess
|
|
1029
|
+
? (data) => { preprocess(data); return interp.validate(data); }
|
|
1030
|
+
: (data) => interp.validate(data));
|
|
1031
|
+
this.validate = run;
|
|
1032
|
+
_bindVerdict(this, this._fastVerdict
|
|
1033
|
+
? this._fastVerdict
|
|
1034
|
+
: (data) => run(data).valid);
|
|
1035
|
+
this.validateJSON = (jsonStr) => {
|
|
1036
|
+
let data;
|
|
1037
|
+
try {
|
|
1038
|
+
data = JSON.parse(jsonStr);
|
|
1039
|
+
} catch (e) {
|
|
1040
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
1041
|
+
return _jsonSyntaxRejection(e);
|
|
1042
|
+
}
|
|
1043
|
+
return run(data);
|
|
1044
|
+
};
|
|
1045
|
+
this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Error presentation, one lazy layer: declaration-order sorting, rich
|
|
1049
|
+
// enrichment (received value, suggestions, source frames, docUrl), or the
|
|
1050
|
+
// raw v0.14 shape under `richErrors: false`. All of it is work a caller
|
|
1051
|
+
// that only reads `.valid` never sees, so it runs on first access to
|
|
1052
|
+
// `.errors` and is cached. One wrapper, one allocation per rejection.
|
|
1053
|
+
if (this.validate) {
|
|
1054
|
+
const inner = this.validate;
|
|
1055
|
+
// Loaded on the first error read rather than at compile: a process that
|
|
1056
|
+
// only ever accepts never needs it.
|
|
1057
|
+
const enrich = this._richErrors ? _enrichLazy : null;
|
|
1058
|
+
const root = this._schemaObj;
|
|
1059
|
+
const self = this;
|
|
1060
|
+
this.validate = (data) => {
|
|
1061
|
+
const result = inner(data);
|
|
1062
|
+
// abortEarly returns the shared ATA9000 stub; preserve it as-is so the
|
|
1063
|
+
// perf fast path stays allocation-free and the documented code stays stable.
|
|
1064
|
+
if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
|
|
1065
|
+
// The raw input travels with the rejection when validateJSON set one.
|
|
1066
|
+
// The map it implies is built on first access to `.errors`, so a
|
|
1067
|
+
// caller reading only `.valid` does not pay for a document walk.
|
|
1068
|
+
const rawInput = enrich ? self._lastRawInput : null;
|
|
1069
|
+
// One instance of a class with prototype accessors. An object
|
|
1070
|
+
// literal with a getter here cost a closure plus an accessor
|
|
1071
|
+
// definition on every rejection, several hundred nanoseconds
|
|
1072
|
+
// before any error was read.
|
|
1073
|
+
return new RichRejection(result, data, rawInput, self, root, enrich);
|
|
1074
|
+
}
|
|
1075
|
+
return result;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// validateJSON also enriches: set _lastRawInput so the position cache
|
|
1079
|
+
// can lazily build a map for dataFrame attachment. Only validateJSON
|
|
1080
|
+
// wires this — validate(data) takes a pre-parsed object, by design.
|
|
1081
|
+
if (this._richErrors && this.validateJSON) {
|
|
1082
|
+
const innerJson = this.validateJSON;
|
|
1083
|
+
this.validateJSON = (jsonStr) => {
|
|
1084
|
+
// The inner path reads _lastRawInput to hand the raw text to the
|
|
1085
|
+
// rejection it builds. Cleared as soon as it returns: the rejection
|
|
1086
|
+
// carries the text itself, so nothing outlives the call.
|
|
1087
|
+
this._lastRawInput = jsonStr;
|
|
1088
|
+
let result;
|
|
1089
|
+
try {
|
|
1090
|
+
result = innerJson(jsonStr);
|
|
1091
|
+
} finally {
|
|
1092
|
+
this._lastRawInput = null;
|
|
1093
|
+
}
|
|
1094
|
+
// Every diagnostic the text path adds is deferred. Deciding here
|
|
1095
|
+
// whether there is anything to add would mean reading `result.errors`,
|
|
1096
|
+
// and on the codegen path that realizes the inner lazy layer, which is
|
|
1097
|
+
// the document walk this exists to avoid.
|
|
1098
|
+
if (result && result.valid === false) {
|
|
1099
|
+
return new LazyJsonRejection(result, jsonStr, this, enrich);
|
|
1100
|
+
}
|
|
1101
|
+
return result;
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// validate() resolves a typed `data` on success: the validated input, after
|
|
1107
|
+
// any in-place coercion/defaults. This matches the ValidationResult<T>
|
|
1108
|
+
// contract. isValidObject() and abortEarly stay allocation-free for hot
|
|
1109
|
+
// paths that only need a boolean.
|
|
1110
|
+
if (this.validate) {
|
|
1111
|
+
const _bare = this.validate;
|
|
1112
|
+
this.validate = fusedRemove
|
|
1113
|
+
// A document the removing verdict accepts is answered here, one call
|
|
1114
|
+
// deep; anything else takes the full path, see fusedRemove above.
|
|
1115
|
+
? (data) => {
|
|
1116
|
+
if (fusedRemove(data)) return { valid: true, data, errors: VALID_RESULT.errors };
|
|
1117
|
+
const r = _bare(data);
|
|
1118
|
+
return (r.valid === true && r.data === undefined)
|
|
1119
|
+
? { valid: true, data, errors: r.errors }
|
|
1120
|
+
: r;
|
|
1121
|
+
}
|
|
1122
|
+
: (data) => {
|
|
1123
|
+
const r = _bare(data);
|
|
1124
|
+
return (r.valid === true && r.data === undefined)
|
|
1125
|
+
? { valid: true, data, errors: r.errors }
|
|
1126
|
+
: r;
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Custom error messages: if any subschema declares an `errorMessage`
|
|
1131
|
+
// keyword, install an outermost decorator that overrides the `message`
|
|
1132
|
+
// field of the errors it owns. Gated on a one-time scan so schemas without
|
|
1133
|
+
// errorMessage keep the validate hot path untouched. Layered after rich
|
|
1134
|
+
// enrichment so `code`/`keyword`/`path` are already final and only the
|
|
1135
|
+
// human-facing message changes.
|
|
1136
|
+
{
|
|
1137
|
+
const emLib = require('./error-messages');
|
|
1138
|
+
const schemaStr = this._schemaStr || (this._schemaObj ? JSON.stringify(this._schemaObj) : '');
|
|
1139
|
+
if (emLib.schemaHasErrorMessages(schemaStr)) {
|
|
1140
|
+
const root = this._schemaObj;
|
|
1141
|
+
const wrap = (inner) => (arg) => {
|
|
1142
|
+
const result = inner(arg);
|
|
1143
|
+
if (result && result.valid === false && result.errors && result.errors.length && result !== ABORT_EARLY_RESULT) {
|
|
1144
|
+
const overridden = emLib.applyErrorMessages(result.errors, root);
|
|
1145
|
+
if (overridden !== result.errors) return { valid: false, errors: overridden };
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
};
|
|
1149
|
+
if (this.validate) this.validate = wrap(this.validate);
|
|
1150
|
+
if (this.validateJSON) this.validateJSON = wrap(this.validateJSON);
|
|
1151
|
+
// validateAndParse routes through self.validate on the codegen path, but
|
|
1152
|
+
// the native-only path returns directly from the addon — wrap it so both
|
|
1153
|
+
// paths get overrides. The result shape carries `value`, preserved here.
|
|
1154
|
+
if (this.validateAndParse) {
|
|
1155
|
+
const innerVP = this.validateAndParse;
|
|
1156
|
+
this.validateAndParse = (arg) => {
|
|
1157
|
+
const result = innerVP(arg);
|
|
1158
|
+
if (result && result.valid === false && result.errors && result.errors.length) {
|
|
1159
|
+
const overridden = emLib.applyErrorMessages(result.errors, root);
|
|
1160
|
+
if (overridden !== result.errors) return { valid: false, value: result.value, errors: overridden };
|
|
1161
|
+
}
|
|
1162
|
+
return result;
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Errors are paid for when read, not when produced. The full pipeline
|
|
1169
|
+
// above (error codegen, enrichment, custom messages, verbose) stays
|
|
1170
|
+
// intact, but validate() now answers the verdict from the boolean
|
|
1171
|
+
// engine and materializes `errors` through a getter on first access.
|
|
1172
|
+
// A caller that only reads `.valid`, which is every gateway check and
|
|
1173
|
+
// every benchmark, skips error construction entirely; a caller that
|
|
1174
|
+
// reads `.errors` pays once and the result is cached. Skipped when the
|
|
1175
|
+
// schema coerces or defaults (preprocess mutates before the verdict),
|
|
1176
|
+
// under abortEarly (already a frozen stub), and for $dynamicRef (the
|
|
1177
|
+
// boolean engine is not the authority there).
|
|
1178
|
+
// A check registered through _extendValidate joins here when it can: the
|
|
1179
|
+
// verdict comes from the generated function with the check compiled in,
|
|
1180
|
+
// and the check's own errors are appended when somebody reads them. A
|
|
1181
|
+
// rejection then costs what it costs without the check. Where this layer
|
|
1182
|
+
// is not installed, the end of this method wraps validate() instead.
|
|
1183
|
+
const _vx = this._validateTail !== null ? this._validateTail() : null;
|
|
1184
|
+
let _vxApplied = false;
|
|
1185
|
+
if (this._fastVerdict && !preprocess && !options.abortEarly && this.validate) {
|
|
1186
|
+
const _full = this.validate;
|
|
1187
|
+
const _fast = _vx ? _fuseTail(this._fastVerdict, _vx.check) : this._fastVerdict;
|
|
1188
|
+
const _extra = _vx ? _vx.errors : null;
|
|
1189
|
+
_vxApplied = true;
|
|
1190
|
+
const EMPTY_ERRORS = Object.freeze([]);
|
|
1191
|
+
const _verdictFallback = [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
|
|
1192
|
+
const _withExtra = (own, data) => {
|
|
1193
|
+
const more = _extra === null ? null : _extra(data);
|
|
1194
|
+
if (more && more.length) return own ? own.concat(more) : more;
|
|
1195
|
+
// The data changed between the verdict and this read; keep the
|
|
1196
|
+
// verdict and say so rather than inventing a specific error.
|
|
1197
|
+
return own || _verdictFallback;
|
|
1198
|
+
};
|
|
1199
|
+
const _buildErrors = (data) => {
|
|
1200
|
+
const r = _full(data);
|
|
1201
|
+
return _withExtra((r && r.valid === false && r.errors && r.errors.length) ? r.errors : null, data);
|
|
1202
|
+
};
|
|
1203
|
+
const _buildRawErrors = (data) => {
|
|
1204
|
+
const r = _full(data);
|
|
1205
|
+
let raw = null;
|
|
1206
|
+
if (r && r.valid === false) {
|
|
1207
|
+
raw = typeof r._ataRaw === 'function' ? r._ataRaw() : r.errors;
|
|
1208
|
+
if (!raw || !raw.length) raw = null;
|
|
1209
|
+
}
|
|
1210
|
+
return _withExtra(raw, data);
|
|
1211
|
+
};
|
|
1212
|
+
this.validate = (data) => {
|
|
1213
|
+
if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
|
|
1214
|
+
return new LazyRejection(_buildErrors, data, _buildRawErrors);
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
if (_vx && !_vxApplied && this.validate) {
|
|
1218
|
+
const inner = this.validate;
|
|
1219
|
+
const { check, errors } = _vx;
|
|
1220
|
+
this.validate = (data) => {
|
|
1221
|
+
const r = inner(data);
|
|
1222
|
+
if (r.valid && check(data)) return r;
|
|
1223
|
+
return new ExtendedRejection(r, data, errors);
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// The scanner wraps whatever answers isValidJSON and validateJSON by now.
|
|
1228
|
+
if (this._jsFn && !this._preprocess) _codegen.installScanner.call(this, schemaObj, options);
|
|
1229
|
+
|
|
1230
|
+
// An extension registered through _extendValidate covers the JSON entry
|
|
1231
|
+
// points too. They are final here except for the scanner stubs above,
|
|
1232
|
+
// which rebind themselves on first use through _bindEntry, so the
|
|
1233
|
+
// extension is applied to whatever each one is now and again on rebind.
|
|
1234
|
+
if (_vx) {
|
|
1235
|
+
this._entryExt = _jsonEntryWrappers(_vx);
|
|
1236
|
+
for (const name of ['validateJSON', 'isValidJSON', 'validateAndParse']) {
|
|
1237
|
+
if (Object.prototype.hasOwnProperty.call(this, name) && typeof this[name] === 'function') _bindEntry(this, name, this[name]);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
_rememberInstance(this);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Which engine answers validate() for this schema: 'codegen' (generated
|
|
1246
|
+
// JS), 'closure' (the closure compiler, the boolean fallback), 'native'
|
|
1247
|
+
// (the C++ engine, only for some $dynamicRef schemas), or 'interpreter'.
|
|
1248
|
+
// A diagnostic: the answer is the same on every engine, the cost is not.
|
|
1249
|
+
engine() {
|
|
1250
|
+
this._ensureCompiled();
|
|
1251
|
+
return this._engine || 'interpreter';
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
_ensureNative() {
|
|
1255
|
+
if (this._nativeReady) return;
|
|
1256
|
+
this._nativeReady = true;
|
|
1257
|
+
const native = getNative();
|
|
1258
|
+
if (!native) return;
|
|
1259
|
+
if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
|
|
1260
|
+
let nativeSchemaStr = this._schemaStr;
|
|
1261
|
+
// A boolean root references nothing, so the registry has nowhere to go and
|
|
1262
|
+
// is left out; merging it used to throw on `true` and `false`.
|
|
1263
|
+
if (this._schemaMap.size > 0 && typeof this._schemaObj === 'object' && this._schemaObj !== null) {
|
|
1264
|
+
const merged = JSON.parse(this._schemaStr);
|
|
1265
|
+
if (!merged.$defs) merged.$defs = {};
|
|
1266
|
+
for (const [id, s] of this._schemaMap) {
|
|
1267
|
+
merged.$defs['__ext_' + id.replace(/[^a-zA-Z0-9]/g, '_')] = s;
|
|
1268
|
+
}
|
|
1269
|
+
nativeSchemaStr = JSON.stringify(merged);
|
|
1270
|
+
}
|
|
1271
|
+
this._compiled = new native.CompiledSchema(nativeSchemaStr);
|
|
1272
|
+
// The fast registry is a fixed array of slots in the addon, and registering
|
|
1273
|
+
// a distinct schema past the last one throws. It is an accelerator for the
|
|
1274
|
+
// buffer path, not a requirement, so a full registry leaves the slot at -1
|
|
1275
|
+
// and the buffer methods answer from the JS engine instead. Letting this
|
|
1276
|
+
// throw made every validator built after the 4096th unusable on that path.
|
|
1277
|
+
try {
|
|
1278
|
+
this._fastSlot = native.fastRegister(nativeSchemaStr);
|
|
1279
|
+
} catch {
|
|
1280
|
+
this._fastSlot = -1;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// The buffer APIs, installed on the first call to any of them. The schema
|
|
1285
|
+
// is compiled first, so the gate sees the final schema and the answers
|
|
1286
|
+
// below never run ahead of validate(). The shapes lib/buffer-gate.js lists,
|
|
1287
|
+
// and a validator the addon had no fast slot left for, answer through
|
|
1288
|
+
// validate() instead of the native walker: the same answers, slower. A
|
|
1289
|
+
// negative slot must never reach rawFastValidate, whose bounds check would
|
|
1290
|
+
// report a valid document as invalid.
|
|
1291
|
+
_installBufferApis(name) {
|
|
1292
|
+
this._ensureCompiled();
|
|
1293
|
+
const native = getNative();
|
|
1294
|
+
if (!native) throw new Error(`Native addon required for ${name}(). Use validate(), isValidObject() or validateJSON(), which do not need it.`);
|
|
1295
|
+
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./buffer-gate.js');
|
|
1296
|
+
if (bufferNeedsSlowPath(this._schemaObj, this._schemaMap, this._keywords)) return installSlowBufferApis(this);
|
|
1297
|
+
this._ensureNative();
|
|
1298
|
+
const slot = this._fastSlot;
|
|
1299
|
+
if (!(slot >= 0)) return installSlowBufferApis(this);
|
|
1300
|
+
const bytes = (b, who) => {
|
|
1301
|
+
if (typeof b === 'string') return Buffer.from(b);
|
|
1302
|
+
if (b instanceof Uint8Array) return b;
|
|
1303
|
+
throw new TypeError(`${who}() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().`);
|
|
1304
|
+
};
|
|
1305
|
+
this.isValid = (b) => native.rawFastValidate(slot, bytes(b, 'isValid'));
|
|
1306
|
+
this.isValidPrepadded = (paddedBuffer, jsonLength) => native.rawFastValidate(slot, paddedBuffer, jsonLength);
|
|
1307
|
+
this.isValidParallel = (b) => native.rawParallelValidate(slot, bytes(b, 'isValidParallel'));
|
|
1308
|
+
this.isValidNDJSON = (b) => native.rawNDJSONValidate(slot, bytes(b, 'isValidNDJSON'));
|
|
1309
|
+
this.countValid = (b) => {
|
|
1310
|
+
const r = native.rawNDJSONValidate(slot, bytes(b, 'countValid'));
|
|
1311
|
+
let n = 0;
|
|
1312
|
+
for (let i = 0; i < r.length; i++) if (r[i]) n++;
|
|
1313
|
+
return n;
|
|
1314
|
+
};
|
|
1315
|
+
this.batchIsValid = (buffers) => {
|
|
1316
|
+
let n = 0;
|
|
1317
|
+
for (const b of buffers) {
|
|
1318
|
+
if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
|
|
1319
|
+
if (native.rawFastValidate(slot, b)) n++;
|
|
1320
|
+
}
|
|
1321
|
+
return n;
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
addSchema(schema) {
|
|
1326
|
+
if (this._initialized) {
|
|
1327
|
+
throw new Error('Cannot add schema after compilation — call addSchema() before validate()')
|
|
1328
|
+
}
|
|
1329
|
+
if (!schema || !schema.$id) {
|
|
1330
|
+
throw new Error('Schema must have $id')
|
|
1331
|
+
}
|
|
1332
|
+
// Normalize a copy so the caller's object is never mutated. A document
|
|
1333
|
+
// without a dialect of its own is read under the root's draft.
|
|
1334
|
+
const root = this._schemaObj
|
|
1335
|
+
const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
|
|
1336
|
+
(root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
|
|
1337
|
+
const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
|
|
1338
|
+
this._ownSchemaMap()
|
|
1339
|
+
this._schemaMap.set(normalized.$id, normalized)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// buildSchemaMap hands the same map to every validator built from the same
|
|
1343
|
+
// registry. Take a private copy before writing to it.
|
|
1344
|
+
_ownSchemaMap() {
|
|
1345
|
+
if (!this._schemaMapShared) return
|
|
1346
|
+
this._schemaMap = new Map(this._schemaMap)
|
|
1347
|
+
this._schemaMapShared = false
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
_ensureCodegen() {
|
|
1351
|
+
if (this._jsFn) return;
|
|
1352
|
+
// A validator that rewrites its input cannot use the binding below: that
|
|
1353
|
+
// one answers from the compiled function alone and would skip the rewrite,
|
|
1354
|
+
// so isValidObject() and validate() would disagree.
|
|
1355
|
+
if (this._needsPreprocess() || this._usesKeywords) {
|
|
1356
|
+
this._ensureCompiled();
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
this._ensureVocabularies();
|
|
1360
|
+
if (this._interpretOnly || _codegen === null || (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI)) return;
|
|
1361
|
+
_codegen.compileVerdict.call(this);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Load a pre-compiled standalone module. Zero schema compilation.
|
|
1365
|
+
// No NAPI, no native compile — pure JS. Startup in microseconds.
|
|
1366
|
+
// Usage: const v = Validator.fromStandalone(require('./compiled.js'), schema, opts)
|
|
1367
|
+
static fromStandalone(mod, schema, opts) {
|
|
1368
|
+
// It loads what Validator.bundleStandalone() writes, and compiles an error
|
|
1369
|
+
// function when the module carries none, so it goes with those methods.
|
|
1370
|
+
if (_codegen === null) throw new TypeError('Validator.fromStandalone is not part of ata-validator/lite, like the bundle methods it loads for. Import it from ata-validator, or import a module from `ata build` directly.');
|
|
1371
|
+
const options = opts || {};
|
|
1372
|
+
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
1373
|
+
|
|
1374
|
+
// Create a lightweight instance — skip NAPI compile entirely
|
|
1375
|
+
const v = Object.create(Validator.prototype);
|
|
1376
|
+
v._jsFn = mod.boolFn;
|
|
1377
|
+
v._compiled = null;
|
|
1378
|
+
v._fastSlot = -1;
|
|
1379
|
+
|
|
1380
|
+
// Mutators
|
|
1381
|
+
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
1382
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
1383
|
+
const applyRemove = options.removeAdditional
|
|
1384
|
+
? buildRemover(schemaObj)
|
|
1385
|
+
: null;
|
|
1386
|
+
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
1387
|
+
const preprocess =
|
|
1388
|
+
mutators.length === 0
|
|
1389
|
+
? null
|
|
1390
|
+
: mutators.length === 1
|
|
1391
|
+
? mutators[0]
|
|
1392
|
+
: (data) => {
|
|
1393
|
+
for (let i = 0; i < mutators.length; i++) mutators[i](data);
|
|
1394
|
+
};
|
|
1395
|
+
v._preprocess = preprocess;
|
|
1396
|
+
|
|
1397
|
+
// Error function — use pre-compiled from standalone if available, else compile
|
|
1398
|
+
let errFn = (d) => ({
|
|
1399
|
+
valid: false,
|
|
1400
|
+
errors: [
|
|
1401
|
+
{ code: "validation_failed", path: "", message: "validation failed" },
|
|
1402
|
+
],
|
|
1403
|
+
});
|
|
1404
|
+
if (mod.errFn) {
|
|
1405
|
+
errFn = (d) => mod.errFn(d, true);
|
|
1406
|
+
} else {
|
|
1407
|
+
const jsErrFn = compileToJSCodegenWithErrors(schemaObj);
|
|
1408
|
+
if (jsErrFn) {
|
|
1409
|
+
try {
|
|
1410
|
+
jsErrFn({}, true);
|
|
1411
|
+
errFn = (d) => jsErrFn(d, true);
|
|
1412
|
+
} catch {}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Hybrid or speculative
|
|
1417
|
+
const hybridFn = mod.hybridFactory
|
|
1418
|
+
? mod.hybridFactory(VALID_RESULT, errFn)
|
|
1419
|
+
: null;
|
|
1420
|
+
|
|
1421
|
+
v.validate = hybridFn
|
|
1422
|
+
? preprocess
|
|
1423
|
+
? (data) => {
|
|
1424
|
+
preprocess(data);
|
|
1425
|
+
return hybridFn(data);
|
|
1426
|
+
}
|
|
1427
|
+
: hybridFn
|
|
1428
|
+
: preprocess
|
|
1429
|
+
? (data) => {
|
|
1430
|
+
preprocess(data);
|
|
1431
|
+
return mod.boolFn(data) ? VALID_RESULT : errFn(data);
|
|
1432
|
+
}
|
|
1433
|
+
: (data) => (mod.boolFn(data) ? VALID_RESULT : errFn(data));
|
|
1434
|
+
{
|
|
1435
|
+
const _bare = v.validate;
|
|
1436
|
+
v.validate = (data) => {
|
|
1437
|
+
const r = _bare(data);
|
|
1438
|
+
return (r.valid === true && r.data === undefined)
|
|
1439
|
+
? { valid: true, data, errors: r.errors }
|
|
1440
|
+
: r;
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
v.isValidObject = mod.boolFn;
|
|
1444
|
+
v.isValidJSON = (jsonStr) => {
|
|
1445
|
+
try {
|
|
1446
|
+
return mod.boolFn(JSON.parse(jsonStr));
|
|
1447
|
+
} catch {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
v.validateJSON = (jsonStr) => {
|
|
1452
|
+
try {
|
|
1453
|
+
const obj = JSON.parse(jsonStr);
|
|
1454
|
+
return hybridFn
|
|
1455
|
+
? hybridFn(obj)
|
|
1456
|
+
: mod.boolFn(obj)
|
|
1457
|
+
? VALID_RESULT
|
|
1458
|
+
: errFn(obj);
|
|
1459
|
+
} catch {
|
|
1460
|
+
return {
|
|
1461
|
+
valid: false,
|
|
1462
|
+
errors: [{ code: "invalid_json", path: "", message: "invalid JSON" }],
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// Standard Schema V1
|
|
1468
|
+
Object.defineProperty(v, "~standard", {
|
|
1469
|
+
value: Object.freeze({
|
|
1470
|
+
version: 1,
|
|
1471
|
+
vendor: "ata-validator",
|
|
1472
|
+
validate(value) {
|
|
1473
|
+
const result = v.validate(value);
|
|
1474
|
+
if (result.valid) return { value };
|
|
1475
|
+
return {
|
|
1476
|
+
issues: result.errors.map((e) => ({
|
|
1477
|
+
message: e.message,
|
|
1478
|
+
path: parsePointerPath(e.instancePath),
|
|
1479
|
+
})),
|
|
1480
|
+
};
|
|
1481
|
+
},
|
|
1482
|
+
}),
|
|
1483
|
+
writable: false,
|
|
1484
|
+
enumerable: false,
|
|
1485
|
+
configurable: false,
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
return v;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// One-shot validate. It goes through a Validator like every other entry
|
|
1493
|
+
// point, so the result has one shape everywhere: `data` on success, errors
|
|
1494
|
+
// with a code, a keyword and an instancePath. From the first native binding
|
|
1495
|
+
// until 1.33.3 it handed the schema straight to the native engine whenever
|
|
1496
|
+
// the addon was loaded, which is every default install on a supported
|
|
1497
|
+
// platform, and returned that engine's raw result: numeric codes, `path`
|
|
1498
|
+
// instead of `instancePath`, no keyword, and no `data`. The compile cache keeps
|
|
1499
|
+
// a schema passed again from compiling again.
|
|
1500
|
+
function validate(schema, data) {
|
|
1501
|
+
if (schema instanceof Validator) return schema.validate(data);
|
|
1502
|
+
const v = new Validator(typeof schema === "string" ? JSON.parse(schema) : schema);
|
|
1503
|
+
return v.validate(data);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Async validation for schemas built with `t.refine(...)`. Structural
|
|
1507
|
+
// validation runs synchronously first; refinements are awaited only when the
|
|
1508
|
+
// value is structurally valid (a refinement body may assume the right shape).
|
|
1509
|
+
// Accepts a schema literal or an existing Validator instance plus its schema.
|
|
1510
|
+
// Returns a Promise<ValidationResult>.
|
|
1511
|
+
async function validateAsync(schemaOrValidator, data) {
|
|
1512
|
+
const refineLib = require('./refine');
|
|
1513
|
+
let validator, schema;
|
|
1514
|
+
if (schemaOrValidator instanceof Validator) {
|
|
1515
|
+
validator = schemaOrValidator;
|
|
1516
|
+
schema = validator._schemaObj;
|
|
1517
|
+
} else {
|
|
1518
|
+
schema = schemaOrValidator;
|
|
1519
|
+
validator = new Validator(schema);
|
|
1520
|
+
}
|
|
1521
|
+
const structural = validator.validate(data);
|
|
1522
|
+
if (!structural.valid) return structural;
|
|
1523
|
+
const refinements = refineLib.getRefinements(schema);
|
|
1524
|
+
if (!refinements) return structural;
|
|
1525
|
+
const issues = await refineLib.runRefinements(refinements, structural.data !== undefined ? structural.data : data);
|
|
1526
|
+
if (issues.length) return { valid: false, errors: issues };
|
|
1527
|
+
return structural;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// parseAsync resolves to the validated data, or rejects with an Error whose
|
|
1531
|
+
// `.errors` carries the ValidationError list. Mirrors the parse/validate split
|
|
1532
|
+
// used by Zod-style callers.
|
|
1533
|
+
async function parseAsync(schemaOrValidator, data) {
|
|
1534
|
+
const result = await validateAsync(schemaOrValidator, data);
|
|
1535
|
+
if (result.valid) return result.data !== undefined ? result.data : data;
|
|
1536
|
+
const err = new Error('ata: async validation failed');
|
|
1537
|
+
err.errors = result.errors;
|
|
1538
|
+
throw err;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function version() {
|
|
1542
|
+
const native = getNative();
|
|
1543
|
+
if (native) return native.version();
|
|
1544
|
+
try { return require("./version"); } catch { return "unknown"; }
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Static AOT entry points are thin lazy-loaders into `lib/aot.js`. The
|
|
1548
|
+
// implementation files (and the `fs`/`path` reads they perform) only enter
|
|
1549
|
+
// the process when one of these is actually called. See `lib/aot.js` for the
|
|
1550
|
+
// generated module shapes; browser bundles get `lib/aot.browser.js` (a stub
|
|
1551
|
+
// that throws) via the package.json `browser` field.
|
|
1552
|
+
function _aot() {
|
|
1553
|
+
if (_codegen === null) throw new TypeError('The ahead-of-time bundle methods are not part of ata-validator/lite. Import them from ata-validator.');
|
|
1554
|
+
return _codegen.aot();
|
|
1555
|
+
}
|
|
1556
|
+
Validator.bundle = function (schemas, opts) {
|
|
1557
|
+
return _aot().bundle(Validator, schemas, opts);
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
Validator.bundleStandalone = function (schemas, opts) {
|
|
1561
|
+
return _aot().bundleStandalone(Validator, schemas, opts);
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
Validator.bundleCompact = function (schemas, opts) {
|
|
1565
|
+
return _aot().bundleCompact(Validator, schemas, opts);
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
Validator.loadBundle = function (mods, schemas, opts) {
|
|
1569
|
+
return _aot().loadBundle(Validator, mods, schemas, opts);
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
// simdjson when the addon is there, JSON.parse otherwise; the addon loads on
|
|
1573
|
+
// the first call rather than when the package is required.
|
|
1574
|
+
function parseJSON(input) {
|
|
1575
|
+
const native = getNative();
|
|
1576
|
+
return native ? native.parseJSON(input) : JSON.parse(input);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Ultra-fast compile: returns validate function directly, no Validator wrapper
|
|
1580
|
+
// WeakMap cached — second call with same schema object is ~3ns
|
|
1581
|
+
const _compileFnCache = new WeakMap();
|
|
1582
|
+
function compile(schema, opts) {
|
|
1583
|
+
if (!opts && typeof schema === 'object' && schema !== null) {
|
|
1584
|
+
const hit = _compileFnCache.get(schema);
|
|
1585
|
+
if (hit) return hit;
|
|
1586
|
+
}
|
|
1587
|
+
const v = new Validator(schema, opts);
|
|
1588
|
+
v._ensureCompiled();
|
|
1589
|
+
const fn = v.validate;
|
|
1590
|
+
if (!opts && typeof schema === 'object' && schema !== null) {
|
|
1591
|
+
_compileFnCache.set(schema, fn);
|
|
1592
|
+
}
|
|
1593
|
+
return fn;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// The renderers, TypeScript generation, the output formats, the retry message
|
|
1597
|
+
// and the suggestion helper are exported by index.js. None of them is called
|
|
1598
|
+
// by a Validator, so ata-validator/lite leaves them out.
|
|
1599
|
+
|
|
1600
|
+
// Authoring helper: identity at runtime. Its only job is to attach the
|
|
1601
|
+
// JSONSchema type (see index.d.ts) to an inline schema object so TypeScript
|
|
1602
|
+
// gives autocomplete and value checking while authoring. Returns the schema
|
|
1603
|
+
// untouched so it can be passed straight to Validator, toStandaloneModule, etc.
|
|
1604
|
+
function defineSchema (schema) {
|
|
1605
|
+
return schema;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Public methods start as memoized accessors on the prototype. A fresh
|
|
1609
|
+
// Validator allocates none of them; the first read of a method builds the
|
|
1610
|
+
// bound closure, stores it on the instance as an ordinary writable property
|
|
1611
|
+
// and returns it. The setter keeps the compile step's plain assignments
|
|
1612
|
+
// (`this.validate = fn`) working before the getter has ever run. Detached
|
|
1613
|
+
// use (`const f = v.validate`) keeps working because the closure binds the
|
|
1614
|
+
// instance.
|
|
1615
|
+
// Standard Schema V1. Built on first read, then pinned to the instance with
|
|
1616
|
+
// the same descriptor the constructor used to install eagerly.
|
|
1617
|
+
Object.defineProperty(Validator.prototype, "~standard", {
|
|
1618
|
+
configurable: true,
|
|
1619
|
+
get() {
|
|
1620
|
+
const self = this;
|
|
1621
|
+
const std = Object.freeze({
|
|
1622
|
+
version: 1,
|
|
1623
|
+
vendor: "ata-validator",
|
|
1624
|
+
validate(value) {
|
|
1625
|
+
const result = self.validate(value);
|
|
1626
|
+
if (result.valid) {
|
|
1627
|
+
return { value };
|
|
1628
|
+
}
|
|
1629
|
+
// An issue carries a message and a path and nothing else, so the
|
|
1630
|
+
// suggestion and source-frame work the rich error path does would be
|
|
1631
|
+
// thrown away here. Take the raw, schema-ordered list when the result
|
|
1632
|
+
// offers one; fall back to the public list otherwise.
|
|
1633
|
+
const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
|
|
1634
|
+
const issues = new Array(raw.length);
|
|
1635
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1636
|
+
const err = raw[i];
|
|
1637
|
+
const path = err.instancePath != null ? err.instancePath : (err.path || '');
|
|
1638
|
+
let message = err.message;
|
|
1639
|
+
if (!message) {
|
|
1640
|
+
// The native engine reports numeric codes without a message; the
|
|
1641
|
+
// enrich pass knows how to word those. Rare, so required lazily.
|
|
1642
|
+
message = require('./enrich-error').enrich(err, {}).message;
|
|
1643
|
+
}
|
|
1644
|
+
issues[i] = { message, path: parsePointerPath(path) };
|
|
1645
|
+
}
|
|
1646
|
+
return { issues };
|
|
1647
|
+
},
|
|
1648
|
+
});
|
|
1649
|
+
Object.defineProperty(this, "~standard", {
|
|
1650
|
+
value: std,
|
|
1651
|
+
writable: false,
|
|
1652
|
+
enumerable: false,
|
|
1653
|
+
configurable: false,
|
|
1654
|
+
});
|
|
1655
|
+
return std;
|
|
1656
|
+
},
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// The error resolvers run only after a verdict function has said no. If one
|
|
1660
|
+
// answers valid anyway, two generators disagree, and the verdict is the one
|
|
1661
|
+
// to keep: returning the resolver's answer is how a vacuous combined function
|
|
1662
|
+
// turned a rejection into an acceptance in validateJSON. The disagreement is
|
|
1663
|
+
// reported as a generic failure rather than hidden.
|
|
1664
|
+
const _VERDICT_DISAGREES = Object.freeze({
|
|
1665
|
+
valid: false,
|
|
1666
|
+
errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
|
|
1667
|
+
});
|
|
1668
|
+
function _mustReject(r) {
|
|
1669
|
+
return r && r.valid === false ? r : _VERDICT_DISAGREES;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Install the verdict method. Every place that binds isValidObject comes
|
|
1673
|
+
// through here, so a check registered with _extendVerdict survives the method
|
|
1674
|
+
// being replaced as the validator compiles further, which it does more than
|
|
1675
|
+
// once over its life.
|
|
1676
|
+
function _bindVerdict(self, fn) {
|
|
1677
|
+
const resolve = self._verdictTail;
|
|
1678
|
+
if (resolve !== null && typeof fn === 'function') {
|
|
1679
|
+
const tail = resolve();
|
|
1680
|
+
if (typeof tail === 'function') fn = _fuseTail(fn, tail);
|
|
1681
|
+
}
|
|
1682
|
+
self.isValidObject = fn;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Bind one of the JSON entry points, through the extension wrapper when there is one.
|
|
1686
|
+
function _bindEntry(self, name, fn) {
|
|
1687
|
+
const ext = self._entryExt;
|
|
1688
|
+
self[name] = ext !== null && ext[name] ? ext[name](fn) : fn;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// The JSON entry points under an extension: the schema answers first, and only
|
|
1692
|
+
// text it accepts is parsed for the check, so a rejection costs nothing extra.
|
|
1693
|
+
function _jsonEntryWrappers({ check, errors }) {
|
|
1694
|
+
const parse = (text) => JSON.parse(typeof text === 'string' ? text : new TextDecoder().decode(text));
|
|
1695
|
+
return {
|
|
1696
|
+
validateJSON: (inner) => (text) => {
|
|
1697
|
+
const res = inner(text);
|
|
1698
|
+
if (!res.valid) return res;
|
|
1699
|
+
let data;
|
|
1700
|
+
try { data = parse(text); } catch { return res; }
|
|
1701
|
+
if (check(data)) return res;
|
|
1702
|
+
const e = errors(data);
|
|
1703
|
+
return e && e.length ? { valid: false, errors: e } : { valid: false, errors: [_EXT_FALLBACK] };
|
|
1704
|
+
},
|
|
1705
|
+
isValidJSON: (inner) => (text) => {
|
|
1706
|
+
if (!inner(text)) return false;
|
|
1707
|
+
let data;
|
|
1708
|
+
try { data = parse(text); } catch { return true; }
|
|
1709
|
+
return check(data);
|
|
1710
|
+
},
|
|
1711
|
+
validateAndParse: (inner) => (text) => {
|
|
1712
|
+
const res = inner(text);
|
|
1713
|
+
if (!res.valid) return res;
|
|
1714
|
+
if (check(res.value)) return res;
|
|
1715
|
+
const e = errors(res.value);
|
|
1716
|
+
return { valid: false, value: res.value, errors: e && e.length ? e : [_EXT_FALLBACK] };
|
|
1717
|
+
},
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
const _EXT_FALLBACK = Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' });
|
|
1721
|
+
|
|
1722
|
+
// A verdict function that also runs `tail` on what it accepts. The generated
|
|
1723
|
+
// function can take the check in place of its final `return true`, one call
|
|
1724
|
+
// per document; anything else is composed.
|
|
1725
|
+
function _fuseTail(fn, tail) {
|
|
1726
|
+
const fused = typeof fn._withTail === 'function' ? fn._withTail(tail) : null;
|
|
1727
|
+
return fused || ((d) => fn(d) && tail(d));
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// The rejection validate() returns on the paths where an extension check could
|
|
1731
|
+
// not join the lazy layer: the inner result, plus the check's errors appended
|
|
1732
|
+
// on first read. `inner` may itself be valid, when only the check failed.
|
|
1733
|
+
class ExtendedRejection {
|
|
1734
|
+
constructor(inner, data, collect) {
|
|
1735
|
+
this.valid = false;
|
|
1736
|
+
this._inner = inner;
|
|
1737
|
+
this._data = data;
|
|
1738
|
+
this._collect = collect;
|
|
1739
|
+
this._errors = null;
|
|
1740
|
+
}
|
|
1741
|
+
toJSON() {
|
|
1742
|
+
return { valid: false, errors: this.errors };
|
|
1743
|
+
}
|
|
1744
|
+
_ataRaw() {
|
|
1745
|
+
const inner = this._inner;
|
|
1746
|
+
const more = this._collect(this._data) || [];
|
|
1747
|
+
const raw = inner.valid ? more : (typeof inner._ataRaw === 'function' ? inner._ataRaw() : inner.errors).concat(more);
|
|
1748
|
+
return raw.length ? raw : [_EXT_FALLBACK];
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
Object.defineProperty(ExtendedRejection.prototype, 'errors', {
|
|
1752
|
+
enumerable: true,
|
|
1753
|
+
configurable: true,
|
|
1754
|
+
get() {
|
|
1755
|
+
if (this._errors === null) {
|
|
1756
|
+
const inner = this._inner;
|
|
1757
|
+
const more = this._collect(this._data) || [];
|
|
1758
|
+
const all = inner.valid ? more : inner.errors.concat(more);
|
|
1759
|
+
this._errors = all.length ? all : [_EXT_FALLBACK];
|
|
1760
|
+
}
|
|
1761
|
+
return this._errors;
|
|
1762
|
+
},
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// For wrappers that enforce a check the schema does not carry, such as the
|
|
1766
|
+
// `instanceof` keyword of @ata-project/keywords. `resolve` is called whenever
|
|
1767
|
+
// the verdict method is bound, which is after the schema has been normalized,
|
|
1768
|
+
// and returns the check, a function of the document that answers true or
|
|
1769
|
+
// false, or null when there is nothing to add. Only isValidObject takes it;
|
|
1770
|
+
// the other entry points, which report errors, stay the wrapper's to handle.
|
|
1771
|
+
//
|
|
1772
|
+
// Before this, such a wrapper had to hold isValidObject behind an accessor so
|
|
1773
|
+
// that the validator's own rebinding could not drop its check, and every call
|
|
1774
|
+
// paid for the accessor and two more calls: 10.1 ns against 4.2 on a document
|
|
1775
|
+
// the schema rejects at its third property.
|
|
1776
|
+
Validator.prototype._verdictTail = null;
|
|
1777
|
+
Validator.prototype._validateTail = null;
|
|
1778
|
+
Validator.prototype._entryExt = null;
|
|
1779
|
+
|
|
1780
|
+
// Let `new Validator(schema)` with the same schema object return this instance.
|
|
1781
|
+
// Only an instance built without options may answer that call: one built with
|
|
1782
|
+
// options (richErrors: false, coerceTypes, formats, ...) would hand its options
|
|
1783
|
+
// to a caller that asked for none, and an extended one would enforce checks the
|
|
1784
|
+
// caller never registered. Both the caller's object and the normalized one are
|
|
1785
|
+
// keys, since a later caller passes the former.
|
|
1786
|
+
function _rememberInstance(self) {
|
|
1787
|
+
if (!self._noOpts || self._verdictTail !== null || self._validateTail !== null) return;
|
|
1788
|
+
const raw = self._rawSchema;
|
|
1789
|
+
if (raw && typeof raw === 'object' && !_identityCache.has(raw)) _identityCache.set(raw, self);
|
|
1790
|
+
const obj = self._schemaObj;
|
|
1791
|
+
if (obj !== raw && obj && typeof obj === 'object' && !_identityCache.has(obj)) _identityCache.set(obj, self);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// An extended validator answers differently from a plain one for the same
|
|
1795
|
+
// schema, so it must not be the instance `new Validator(sameSchema)` hands out.
|
|
1796
|
+
function _leaveIdentityCache(self) {
|
|
1797
|
+
self._noOpts = false;
|
|
1798
|
+
// Nothing is registered before the first compile, so there is nothing to
|
|
1799
|
+
// take back.
|
|
1800
|
+
if (!self._initialized && self._jsFn === null) return;
|
|
1801
|
+
const raw = self._rawSchema;
|
|
1802
|
+
if (raw && typeof raw === 'object' && _identityCache.get(raw) === self) _identityCache.delete(raw);
|
|
1803
|
+
// The compiled form is cached too, at the end of the first compile. Read it
|
|
1804
|
+
// only if it is already materialized: reading it otherwise builds it.
|
|
1805
|
+
if (Object.prototype.hasOwnProperty.call(self, '_schemaObj')) {
|
|
1806
|
+
const obj = self._schemaObj;
|
|
1807
|
+
if (obj && typeof obj === 'object' && _identityCache.get(obj) === self) _identityCache.delete(obj);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// The same kind of extension for validate(): `resolve` returns { check, errors }
|
|
1812
|
+
// or null, where `errors(data)` lists the check's own errors, or returns null
|
|
1813
|
+
// when there are none. The check's errors come after the schema's, and a value
|
|
1814
|
+
// that fails only the check is rejected with the check's errors alone. Must be
|
|
1815
|
+
// called before validate() is first used, which is when it is compiled; later
|
|
1816
|
+
// calls throw rather than being silently ignored.
|
|
1817
|
+
Validator.prototype._extendValidate = function (resolve) {
|
|
1818
|
+
if (typeof resolve !== 'function') throw new TypeError('_extendValidate expects a function');
|
|
1819
|
+
if (this._initialized) throw new Error('_extendValidate must be called before the validator compiles');
|
|
1820
|
+
_leaveIdentityCache(this);
|
|
1821
|
+
const prev = this._validateTail;
|
|
1822
|
+
this._validateTail = prev === null ? resolve : () => {
|
|
1823
|
+
const a = prev(), b = resolve();
|
|
1824
|
+
if (!a) return b;
|
|
1825
|
+
if (!b) return a;
|
|
1826
|
+
return {
|
|
1827
|
+
check: (d) => a.check(d) && b.check(d),
|
|
1828
|
+
errors: (d) => {
|
|
1829
|
+
const x = a.errors(d), y = b.errors(d);
|
|
1830
|
+
if (!x) return y;
|
|
1831
|
+
if (!y) return x;
|
|
1832
|
+
return x.concat(y);
|
|
1833
|
+
},
|
|
1834
|
+
};
|
|
1835
|
+
};
|
|
1836
|
+
return this;
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
// Both extensions in one call, from one resolver that returns { check, errors }
|
|
1840
|
+
// or null. This is the form @ata-project/keywords uses: registering costs one
|
|
1841
|
+
// closure, where wrapping the five entry points cost a closure per entry point,
|
|
1842
|
+
// the wrappers themselves and an accessor, most of what building a wrapped
|
|
1843
|
+
// validator took.
|
|
1844
|
+
// parse(data): validate, then return a copy holding only what the schema
|
|
1845
|
+
// declares, the way the parse() export of an ahead-of-time module does, with
|
|
1846
|
+
// the same emitter behind both. Building the copy from the schema's own key
|
|
1847
|
+
// list costs less than finding and deleting unknown keys, and leaves the
|
|
1848
|
+
// caller's object alone. Where the key set cannot be proven (a $ref it cannot
|
|
1849
|
+
// inline, patternProperties, an open object) the method declines with an
|
|
1850
|
+
// error instead of guessing, as the module ships no parse() there; so it does
|
|
1851
|
+
// under options that rewrite input, whose answers a copy would not reproduce.
|
|
1852
|
+
Validator.prototype.parse = function (data) {
|
|
1853
|
+
let fn = this._parseFn;
|
|
1854
|
+
if (fn === undefined) {
|
|
1855
|
+
fn = _buildParse(this);
|
|
1856
|
+
Object.defineProperty(this, '_parseFn', { value: fn, writable: true, configurable: true, enumerable: false });
|
|
1857
|
+
}
|
|
1858
|
+
return fn(data);
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
function _buildParse(self) {
|
|
1862
|
+
const decline = (why, instead = 'Use validate() with removeAdditional instead.') => () => {
|
|
1863
|
+
throw new TypeError(`parse() is not available for this validator: ${why}. ${instead}`);
|
|
1864
|
+
};
|
|
1865
|
+
const o = self._options;
|
|
1866
|
+
// Each refusal names its own reason: a caller who hit the combined one could
|
|
1867
|
+
// not tell a coercion option from a keyword package, and read a deliberate
|
|
1868
|
+
// refusal as a failure of valid data.
|
|
1869
|
+
if (o.coerceTypes) return decline('coerceTypes rewrites the input before it is checked');
|
|
1870
|
+
if (o.removeAdditional === 'all') return decline("removeAdditional: 'all' decides the kept keys at check time");
|
|
1871
|
+
if (self._usesKeywords) return decline('custom keywords are in use, and what they accept is not known to the copy');
|
|
1872
|
+
// Checks added to the validator (withKeywords from @ata-project/keywords
|
|
1873
|
+
// adds instanceof and typeof) only narrow what passes; they do not change
|
|
1874
|
+
// which keys the schema declares. The verdict then goes through
|
|
1875
|
+
// isValidObject, which runs them, and a property they check with instanceof
|
|
1876
|
+
// is carried over as it is (see clone-emit).
|
|
1877
|
+
const extended = self._verdictTail !== null || self._validateTail !== null;
|
|
1878
|
+
if (_codegen === null) return decline('ata-validator/lite has no code generator to build the copy with', 'Use parse() from ata-validator, or validate(); a valid result carries the input as it is.');
|
|
1879
|
+
return _codegen.buildParse(self, decline, extended);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
Validator.prototype._extendChecks = function (resolve) {
|
|
1883
|
+
if (typeof resolve !== 'function') throw new TypeError('_extendChecks expects a function');
|
|
1884
|
+
this._extendValidate(resolve);
|
|
1885
|
+
return this._extendVerdict(() => {
|
|
1886
|
+
const x = resolve();
|
|
1887
|
+
return x ? x.check : null;
|
|
1888
|
+
});
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
// Whether this instance enforces a check its schema does not carry. The
|
|
1892
|
+
// ahead-of-time emitters build a module from the schema alone, so they refuse
|
|
1893
|
+
// an instance that says yes rather than emit one that accepts too much.
|
|
1894
|
+
// Reading it resolves the registered checks; only an emitter reads it.
|
|
1895
|
+
Object.defineProperty(Validator.prototype, '_externalChecks', {
|
|
1896
|
+
configurable: true,
|
|
1897
|
+
get() {
|
|
1898
|
+
if (this._validateTail !== null && this._validateTail()) return true;
|
|
1899
|
+
if (this._verdictTail !== null && typeof this._verdictTail() === 'function') return true;
|
|
1900
|
+
return false;
|
|
1901
|
+
},
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
Validator.prototype._extendVerdict = function (resolve) {
|
|
1905
|
+
if (typeof resolve !== 'function') throw new TypeError('_extendVerdict expects a function');
|
|
1906
|
+
_leaveIdentityCache(this);
|
|
1907
|
+
const prev = this._verdictTail;
|
|
1908
|
+
this._verdictTail = prev === null ? resolve : () => {
|
|
1909
|
+
const a = prev(), b = resolve();
|
|
1910
|
+
if (typeof a !== 'function') return b;
|
|
1911
|
+
if (typeof b !== 'function') return a;
|
|
1912
|
+
return (d) => a(d) && b(d);
|
|
1913
|
+
};
|
|
1914
|
+
// A method bound before this call was bound without the check. Rebind it.
|
|
1915
|
+
// An unbound one binds through _bindVerdict on its first call.
|
|
1916
|
+
if (Object.prototype.hasOwnProperty.call(this, 'isValidObject')) _bindVerdict(this, this.isValidObject);
|
|
1917
|
+
return this;
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
function _defineLazyMethod(name, maker) {
|
|
1921
|
+
Object.defineProperty(Validator.prototype, name, {
|
|
1922
|
+
configurable: true,
|
|
1923
|
+
get() {
|
|
1924
|
+
const fn = maker(this);
|
|
1925
|
+
Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
|
|
1926
|
+
return fn;
|
|
1927
|
+
},
|
|
1928
|
+
set(fn) {
|
|
1929
|
+
Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
|
|
1930
|
+
},
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
for (const [name, pick] of [
|
|
1935
|
+
['_schemaObj', (self) => _materializeSchema(self)],
|
|
1936
|
+
['_usesKeywords', (self) => { _materializeSchema(self); return self._usesKeywords; }],
|
|
1937
|
+
['_schemaIsCallers', (self) => { _materializeSchema(self); return self._schemaIsCallers; }],
|
|
1938
|
+
]) {
|
|
1939
|
+
Object.defineProperty(Validator.prototype, name, {
|
|
1940
|
+
configurable: true,
|
|
1941
|
+
get() { return pick(this); },
|
|
1942
|
+
set(v) { Object.defineProperty(this, name, { value: v, writable: true, configurable: true, enumerable: true }); },
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
_defineLazyMethod('validate', (self) => (data) => {
|
|
1947
|
+
self._ensureCompiled();
|
|
1948
|
+
return self.validate(data);
|
|
1949
|
+
});
|
|
1950
|
+
_defineLazyMethod('isValidObject', (self) => (data) => {
|
|
1951
|
+
// A validator that rewrites its input goes through the full compile, which
|
|
1952
|
+
// binds a verdict method that runs the rewrite first. So does one whose
|
|
1953
|
+
// schema uses a custom keyword: neither the tier-0 plan nor the code
|
|
1954
|
+
// generator knows the keyword, and either would accept what validate()
|
|
1955
|
+
// rejects.
|
|
1956
|
+
if (self._needsPreprocess() || self._usesKeywords) {
|
|
1957
|
+
self._ensureCompiled();
|
|
1958
|
+
return self.isValidObject(data);
|
|
1959
|
+
}
|
|
1960
|
+
// Lazy: classify + build tier 0 plan on first call, not in constructor.
|
|
1961
|
+
const _tier = classify(self._schemaObj);
|
|
1962
|
+
if (_tier.tier === 0) {
|
|
1963
|
+
const _plan = buildTier0Plan(self._schemaObj);
|
|
1964
|
+
let _n = 0;
|
|
1965
|
+
_bindVerdict(self, (d) => {
|
|
1966
|
+
const r = tier0Validate(_plan, d);
|
|
1967
|
+
if (++_n === 2) {
|
|
1968
|
+
try { self._ensureCodegen(); } catch {}
|
|
1969
|
+
}
|
|
1970
|
+
return r;
|
|
1971
|
+
});
|
|
1972
|
+
} else {
|
|
1973
|
+
// `new Function` is a property of the realm, not of the schema: under a
|
|
1974
|
+
// strict CSP or `--disallow-code-generation-from-strings` this throws
|
|
1975
|
+
// rather than declining, and the EvalError reached the caller of a verdict
|
|
1976
|
+
// method. The full compile can answer without code generation, so fall
|
|
1977
|
+
// through to it. The tier-0 branch above already guarded its own call.
|
|
1978
|
+
try { self._ensureCodegen(); } catch { /* no codegen in this realm */ }
|
|
1979
|
+
// Codegen can bail on shapes it cannot represent; the full compile
|
|
1980
|
+
// binds the native path or the unsupported thrower instead of
|
|
1981
|
+
// leaving this stub to re-dispatch to itself.
|
|
1982
|
+
if (!self._jsFn) self._ensureCompiled();
|
|
1983
|
+
}
|
|
1984
|
+
return self.isValidObject(data);
|
|
1985
|
+
});
|
|
1986
|
+
_defineLazyMethod('validateJSON', (self) => (jsonStr) => {
|
|
1987
|
+
self._ensureCompiled();
|
|
1988
|
+
return self.validateJSON(jsonStr);
|
|
1989
|
+
});
|
|
1990
|
+
_defineLazyMethod('isValidJSON', (self) => (jsonStr) => {
|
|
1991
|
+
self._ensureCompiled();
|
|
1992
|
+
return self.isValidJSON(jsonStr);
|
|
1993
|
+
});
|
|
1994
|
+
// Parse, then validate: JSON.parse and validate(), so it answers the same with
|
|
1995
|
+
// or without the addon and in a browser. The addon's own validateAndParse was
|
|
1996
|
+
// used where there was no code generator, and needed the addon for nothing
|
|
1997
|
+
// validate() cannot do.
|
|
1998
|
+
_defineLazyMethod('validateAndParse', (self) => (jsonStr) => {
|
|
1999
|
+
let value;
|
|
2000
|
+
try {
|
|
2001
|
+
value = JSON.parse(typeof jsonStr === 'string' ? jsonStr : new TextDecoder().decode(jsonStr));
|
|
2002
|
+
} catch (e) {
|
|
2003
|
+
return { valid: false, value: undefined, errors: [{ code: 'ATA9001', message: 'invalid JSON: ' + e.message, keyword: '__parse__', instancePath: '', schemaPath: '', params: {} }] };
|
|
2004
|
+
}
|
|
2005
|
+
const r = self.validate(value);
|
|
2006
|
+
return { valid: r.valid, value, errors: r.errors };
|
|
2007
|
+
});
|
|
2008
|
+
for (const name of ['isValid', 'isValidPrepadded', 'isValidParallel', 'isValidNDJSON', 'countValid', 'batchIsValid']) {
|
|
2009
|
+
_defineLazyMethod(name, (self) => (...args) => {
|
|
2010
|
+
self._installBufferApis(name);
|
|
2011
|
+
return self[name](...args);
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
module.exports = {
|
|
2016
|
+
Validator,
|
|
2017
|
+
compile,
|
|
2018
|
+
validate,
|
|
2019
|
+
validateAsync,
|
|
2020
|
+
parseAsync,
|
|
2021
|
+
version,
|
|
2022
|
+
createPaddedBuffer,
|
|
2023
|
+
SIMDJSON_PADDING,
|
|
2024
|
+
parseJSON,
|
|
2025
|
+
defineSchema,
|
|
2026
|
+
};
|
|
2027
|
+
// How an entry registers the code generator. Not enumerable, so the exported
|
|
2028
|
+
// surface of `require('ata-validator')` is what it was.
|
|
2029
|
+
Object.defineProperty(module.exports, '_registerCodegen', { value: _registerCodegen, enumerable: false });
|
|
2030
|
+
// What the compiled paths in index.js need from this module.
|
|
2031
|
+
Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _mustReject, _rememberInstance, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
|