ata-validator 1.31.2 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +10 -0
- package/index.js +113 -16
- package/lib/aot-impl.js +1 -271
- package/lib/clone-emit.js +302 -0
- package/lib/interpreter.js +10 -4
- package/lib/js-compiler.js +155 -21
- package/lib/plan-compiler.js +1 -1
- package/lib/plan-source.js +281 -0
- package/lib/tier0.js +2 -2
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/index.d.ts
CHANGED
|
@@ -497,6 +497,16 @@ export interface Validator<T = unknown> {
|
|
|
497
497
|
/** Parse JSON and validate against the schema. Returns the parsed value and the validation result. Works without the native addon. */
|
|
498
498
|
validateAndParse(jsonString: string | Buffer): ValidateAndParseResult<T>;
|
|
499
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Validate, then return a copy holding only the properties the schema declares, with defaults
|
|
502
|
+
* filled. The input is not modified. Throws an error named `AtaValidationError`, with `errors`,
|
|
503
|
+
* when the value is invalid, and a `TypeError` where the kept key set cannot be proven from the
|
|
504
|
+
* schema (a $ref it cannot inline, patternProperties, an object that allows unknown keys) or the
|
|
505
|
+
* validator rewrites input (`coerceTypes`, custom keywords). Same result as the `parse` export of
|
|
506
|
+
* a module built with `ata build`.
|
|
507
|
+
*/
|
|
508
|
+
parse(data: unknown): T;
|
|
509
|
+
|
|
500
510
|
/** Ultra-fast buffer validation via native addon */
|
|
501
511
|
isValid(input: Buffer | Uint8Array | string): boolean;
|
|
502
512
|
|
package/index.js
CHANGED
|
@@ -244,26 +244,36 @@ function collectRemovals(schema, actions, path) {
|
|
|
244
244
|
// Emit the in-place strip for one schema node and everything under its
|
|
245
245
|
// `properties`. Scope matches collectRemovals(): object properties only, so
|
|
246
246
|
// the two paths keep the same answer.
|
|
247
|
+
// Each level's code, its own removal loop and its children's, sits inside the
|
|
248
|
+
// guard for that level. A child used to be guarded only on itself, so its
|
|
249
|
+
// access expression read through a parent that could be absent or null:
|
|
250
|
+
// `{ c: { properties: { d: { additionalProperties: false } } } }` threw a
|
|
251
|
+
// TypeError from validate({}) whenever the optional `c` was missing.
|
|
247
252
|
function emitRemovals(node, access, lines, depth, seen) {
|
|
248
253
|
if (!node || typeof node !== 'object' || !node.properties) return;
|
|
249
254
|
if (seen.has(node)) return;
|
|
250
255
|
seen.add(node);
|
|
251
256
|
const keys = Object.keys(node.properties);
|
|
252
|
-
|
|
257
|
+
const body = [];
|
|
258
|
+
// With no declared property every key is additional, as the interpreted
|
|
259
|
+
// engine's remover has it; this pass used to skip such a node entirely.
|
|
260
|
+
if (node.additionalProperties === false) {
|
|
253
261
|
const kv = '_k' + depth;
|
|
254
262
|
const checks = keys.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&');
|
|
255
|
-
|
|
256
|
-
?
|
|
257
|
-
: `
|
|
258
|
-
lines.push(`${guard}for(var ${kv} in ${access})if(${checks})delete ${access}[${kv}]`);
|
|
263
|
+
body.push(keys.length > 0
|
|
264
|
+
? `for(var ${kv} in ${access})if(${checks})delete ${access}[${kv}]`
|
|
265
|
+
: `for(var ${kv} in ${access})delete ${access}[${kv}]`);
|
|
259
266
|
}
|
|
260
267
|
for (const key of keys) {
|
|
261
268
|
const prop = node.properties[key];
|
|
262
269
|
if (prop && typeof prop === 'object' && prop.properties) {
|
|
263
|
-
emitRemovals(prop, `${access}[${JSON.stringify(key)}]`,
|
|
270
|
+
emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, body, depth + 1, seen);
|
|
264
271
|
}
|
|
265
272
|
}
|
|
266
273
|
seen.delete(node);
|
|
274
|
+
if (body.length === 0) return;
|
|
275
|
+
if (depth === 0) lines.push(...body);
|
|
276
|
+
else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
// Generate a fast preprocess function via codegen instead of closure arrays
|
|
@@ -1306,6 +1316,23 @@ class Validator {
|
|
|
1306
1316
|
this._mutatesInput = !!(preprocess || options.coerceTypes || options.removeAdditional);
|
|
1307
1317
|
this._preprocess = preprocess;
|
|
1308
1318
|
|
|
1319
|
+
// removeAdditional alone, the common parse-and-strip use: a verdict function
|
|
1320
|
+
// that deletes unknown keys in the walk it already makes, where the pass
|
|
1321
|
+
// above walks every object a second time just to find them. It answers
|
|
1322
|
+
// the documents it accepts; anything it rejects takes the full path below,
|
|
1323
|
+
// which removes, validates and reports exactly as before, so a rejected
|
|
1324
|
+
// document is left as clean as it always was. The generator declines any
|
|
1325
|
+
// schema where deleting during the walk could change an answer.
|
|
1326
|
+
let fusedRemove = null;
|
|
1327
|
+
if (preprocess && options.removeAdditional && !options.coerceTypes && !this._interpretOnly &&
|
|
1328
|
+
!this._userFormats && !this._usesKeywords && !this._schemaStr.includes('"default"')) {
|
|
1329
|
+
try {
|
|
1330
|
+
fusedRemove = compileToJSCodegen(schemaObj, this._schemaMap.size > 0 ? this._schemaMap : null, null, { removeAdditional: true });
|
|
1331
|
+
} catch {
|
|
1332
|
+
fusedRemove = null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1309
1336
|
// Detect if schema is "selective" -- doesn't recurse into arrays/deep objects.
|
|
1310
1337
|
const hasArrayTraversal =
|
|
1311
1338
|
schemaObj &&
|
|
@@ -1577,9 +1604,11 @@ class Validator {
|
|
|
1577
1604
|
// The verdict methods answer validate()'s question without building the
|
|
1578
1605
|
// error list, so they run the same preprocess pass. Skipping it made the
|
|
1579
1606
|
// two disagree on input that coercion or a default would have fixed.
|
|
1580
|
-
_bindVerdict(this,
|
|
1581
|
-
? (data) =>
|
|
1582
|
-
:
|
|
1607
|
+
_bindVerdict(this, fusedRemove
|
|
1608
|
+
? (data) => fusedRemove(data) || (preprocess(data), jsFn(data))
|
|
1609
|
+
: preprocess
|
|
1610
|
+
? (data) => { preprocess(data); return jsFn(data) }
|
|
1611
|
+
: jsFn);
|
|
1583
1612
|
// Same preference as the object path: the combined function first, since
|
|
1584
1613
|
// it validates and collects in one pass, and the error generator behind
|
|
1585
1614
|
// it. `errPreferCombined` is that order, resolved on the first rejection
|
|
@@ -1794,7 +1823,12 @@ class Validator {
|
|
|
1794
1823
|
: _validate;
|
|
1795
1824
|
_bindVerdict(this, this._fastVerdict
|
|
1796
1825
|
? this._fastVerdict
|
|
1797
|
-
|
|
1826
|
+
// The rewrite runs first here too, as it does for validate() above; a
|
|
1827
|
+
// verdict without it disagreed with validate() on input that coercion,
|
|
1828
|
+
// a default or a removed key would have fixed.
|
|
1829
|
+
: preprocess
|
|
1830
|
+
? (data) => { preprocess(data); return _validate(data).valid; }
|
|
1831
|
+
: (data) => _validate(data).valid);
|
|
1798
1832
|
this.validateAndParse = (jsonStr) => this._compiled.validateAndParse(jsonStr);
|
|
1799
1833
|
{
|
|
1800
1834
|
const slot = this._fastSlot;
|
|
@@ -1927,12 +1961,22 @@ class Validator {
|
|
|
1927
1961
|
// paths that only need a boolean.
|
|
1928
1962
|
if (this.validate) {
|
|
1929
1963
|
const _bare = this.validate;
|
|
1930
|
-
this.validate =
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1964
|
+
this.validate = fusedRemove
|
|
1965
|
+
// A document the removing verdict accepts is answered here, one call
|
|
1966
|
+
// deep; anything else takes the full path, see fusedRemove above.
|
|
1967
|
+
? (data) => {
|
|
1968
|
+
if (fusedRemove(data)) return { valid: true, data, errors: VALID_RESULT.errors };
|
|
1969
|
+
const r = _bare(data);
|
|
1970
|
+
return (r.valid === true && r.data === undefined)
|
|
1971
|
+
? { valid: true, data, errors: r.errors }
|
|
1972
|
+
: r;
|
|
1973
|
+
}
|
|
1974
|
+
: (data) => {
|
|
1975
|
+
const r = _bare(data);
|
|
1976
|
+
return (r.valid === true && r.data === undefined)
|
|
1977
|
+
? { valid: true, data, errors: r.errors }
|
|
1978
|
+
: r;
|
|
1979
|
+
};
|
|
1936
1980
|
}
|
|
1937
1981
|
|
|
1938
1982
|
// Custom error messages: if any subschema declares an `errorMessage`
|
|
@@ -2851,6 +2895,59 @@ Validator.prototype._extendValidate = function (resolve) {
|
|
|
2851
2895
|
// closure, where wrapping the five entry points cost a closure per entry point,
|
|
2852
2896
|
// the wrappers themselves and an accessor, most of what building a wrapped
|
|
2853
2897
|
// validator took.
|
|
2898
|
+
// parse(data): validate, then return a copy holding only what the schema
|
|
2899
|
+
// declares, the way the parse() export of an ahead-of-time module does, with
|
|
2900
|
+
// the same emitter behind both. Building the copy from the schema's own key
|
|
2901
|
+
// list costs less than finding and deleting unknown keys, and leaves the
|
|
2902
|
+
// caller's object alone. Where the key set cannot be proven (a $ref it cannot
|
|
2903
|
+
// inline, patternProperties, an open object) the method declines with an
|
|
2904
|
+
// error instead of guessing, as the module ships no parse() there; so it does
|
|
2905
|
+
// under options that rewrite input, whose answers a copy would not reproduce.
|
|
2906
|
+
Validator.prototype.parse = function (data) {
|
|
2907
|
+
let fn = this._parseFn;
|
|
2908
|
+
if (fn === undefined) {
|
|
2909
|
+
fn = _buildParse(this);
|
|
2910
|
+
Object.defineProperty(this, '_parseFn', { value: fn, writable: true, configurable: true, enumerable: false });
|
|
2911
|
+
}
|
|
2912
|
+
return fn(data);
|
|
2913
|
+
};
|
|
2914
|
+
|
|
2915
|
+
function _buildParse(self) {
|
|
2916
|
+
const decline = (why) => () => {
|
|
2917
|
+
throw new TypeError(`parse() is not available for this validator: ${why}. Use validate() with removeAdditional instead.`);
|
|
2918
|
+
};
|
|
2919
|
+
const o = self._options;
|
|
2920
|
+
if (o.coerceTypes || o.removeAdditional === 'all' || self._usesKeywords || self._verdictTail !== null || self._validateTail !== null) {
|
|
2921
|
+
return decline('its options or keywords rewrite or extend what the schema checks');
|
|
2922
|
+
}
|
|
2923
|
+
const { cloneExprFor } = require('./lib/clone-emit');
|
|
2924
|
+
const expr = cloneExprFor(self._schemaObj);
|
|
2925
|
+
if (!expr) return decline('the set of keys to keep cannot be proven from the schema');
|
|
2926
|
+
let copy;
|
|
2927
|
+
try {
|
|
2928
|
+
// eslint-disable-next-line no-new-func
|
|
2929
|
+
copy = new Function('data', 'return ' + expr);
|
|
2930
|
+
} catch {
|
|
2931
|
+
return decline('code generation is not allowed here');
|
|
2932
|
+
}
|
|
2933
|
+
self._ensureCompiled();
|
|
2934
|
+
const verdict = self._jsFn;
|
|
2935
|
+
if (typeof verdict !== 'function') return decline('the schema has no generated verdict function');
|
|
2936
|
+
return (data) => {
|
|
2937
|
+
if (!verdict(data)) {
|
|
2938
|
+
const e = new Error('validation failed');
|
|
2939
|
+
e.name = 'AtaValidationError';
|
|
2940
|
+
let target = data;
|
|
2941
|
+
if (self._mutatesInput) {
|
|
2942
|
+
try { target = structuredClone(data); } catch { target = data; }
|
|
2943
|
+
}
|
|
2944
|
+
e.errors = self.validate(target).errors;
|
|
2945
|
+
throw e;
|
|
2946
|
+
}
|
|
2947
|
+
return copy(data);
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2854
2951
|
Validator.prototype._extendChecks = function (resolve) {
|
|
2855
2952
|
if (typeof resolve !== 'function') throw new TypeError('_extendChecks expects a function');
|
|
2856
2953
|
this._extendValidate(resolve);
|
package/lib/aot-impl.js
CHANGED
|
@@ -189,277 +189,7 @@ function emitFormatDecls(closures, mode, declKW) {
|
|
|
189
189
|
return { decls: out, exportsSetFormats: false };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
// declares, dropping everything else. This is the sanitising counterpart to
|
|
194
|
-
// removeAdditional: instead of enumerating the input and deleting the keys
|
|
195
|
-
// that do not belong, it reads only the keys that do. That is O(schema)
|
|
196
|
-
// rather than O(input), it leaves the caller's object untouched, and it keeps
|
|
197
|
-
// one hidden class instead of walking the object through a transition per
|
|
198
|
-
// delete.
|
|
199
|
-
//
|
|
200
|
-
// Returns null when the clone cannot be proven exact, and the module then
|
|
201
|
-
// simply has no parse(). Declining is recoverable; a sanitiser that quietly
|
|
202
|
-
// drops a property the schema allows is not.
|
|
203
|
-
// Resolve a local JSON pointer ('#', '#/$defs/x', draft-7 '#/definitions/x',
|
|
204
|
-
// any '#/...' path) against the schema root. Returns null for anything else:
|
|
205
|
-
// external documents, anchors, URNs. Those stay with the runtime engines.
|
|
206
|
-
function resolveLocalPointer(root, ref) {
|
|
207
|
-
if (ref === '#') return root;
|
|
208
|
-
if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
|
|
209
|
-
let cur = root;
|
|
210
|
-
for (const raw of ref.slice(2).split('/')) {
|
|
211
|
-
let seg = raw.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
212
|
-
if (cur === null || typeof cur !== 'object') return null;
|
|
213
|
-
if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
|
|
214
|
-
try { seg = decodeURIComponent(seg); } catch (_) { return null; }
|
|
215
|
-
if (!Object.prototype.hasOwnProperty.call(cur, seg)) return null;
|
|
216
|
-
}
|
|
217
|
-
cur = cur[seg];
|
|
218
|
-
}
|
|
219
|
-
return cur;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Inline local, acyclic $refs before the clone proof runs, so a generated
|
|
223
|
-
// schema ($defs + $ref, the shape every schema generator emits) can still get
|
|
224
|
-
// a parse(). Only a node that is exactly a $ref plus annotations is replaced;
|
|
225
|
-
// a $ref with a constraining sibling, a cycle, an unresolvable target or an
|
|
226
|
-
// external reference is left in place, and emitClone declines it as before.
|
|
227
|
-
// The walk never mutates its input and never descends into data positions
|
|
228
|
-
// (default, const, enum, examples), where an object holding a "$ref" key is
|
|
229
|
-
// a value, not a schema.
|
|
230
|
-
const _INLINE_ANNOTATIONS = new Set(['$ref', 'title', 'description', '$comment', 'examples', 'deprecated', 'readOnly', 'writeOnly', 'default']);
|
|
231
|
-
const _INLINE_DATA_KEYS = new Set(['default', 'const', 'enum', 'examples']);
|
|
232
|
-
function inlineRefsForClone(root) {
|
|
233
|
-
const state = { budget: 512 };
|
|
234
|
-
// `inRef` marks content brought in from behind a reference. The runtime's
|
|
235
|
-
// useDefaults fills a default written next to the $ref, and does not fill
|
|
236
|
-
// defaults written inside the referenced definition, so the inlined copy
|
|
237
|
-
// keeps the first and drops the second. parse() must stay exactly as
|
|
238
|
-
// generous as validate(); a parse that fills more is a disagreement, not a
|
|
239
|
-
// feature.
|
|
240
|
-
const walk = (node, active, inRef) => {
|
|
241
|
-
if (!node || typeof node !== 'object') return node;
|
|
242
|
-
if (Array.isArray(node)) return node.map((n) => walk(n, active, inRef));
|
|
243
|
-
let cur = node;
|
|
244
|
-
let entered = false;
|
|
245
|
-
let siblingDefault;
|
|
246
|
-
while (cur && typeof cur === 'object' && !Array.isArray(cur) && typeof cur.$ref === 'string') {
|
|
247
|
-
for (const k of Object.keys(cur)) if (!_INLINE_ANNOTATIONS.has(k)) return node;
|
|
248
|
-
if (active.has(cur.$ref) || state.budget-- <= 0) return node;
|
|
249
|
-
const target = resolveLocalPointer(root, cur.$ref);
|
|
250
|
-
if (!target || typeof target !== 'object' || Array.isArray(target)) return node;
|
|
251
|
-
if (!entered && !inRef && cur.default !== undefined) siblingDefault = cur.default;
|
|
252
|
-
active = new Set(active);
|
|
253
|
-
active.add(cur.$ref);
|
|
254
|
-
cur = target;
|
|
255
|
-
entered = true;
|
|
256
|
-
}
|
|
257
|
-
const strip = inRef || entered;
|
|
258
|
-
const out = {};
|
|
259
|
-
for (const k of Object.keys(cur)) {
|
|
260
|
-
if (strip && k === 'default') continue;
|
|
261
|
-
const v = cur[k];
|
|
262
|
-
if (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') { out[k] = v; continue; }
|
|
263
|
-
out[k] = walk(v, active, strip);
|
|
264
|
-
}
|
|
265
|
-
if (siblingDefault !== undefined) out.default = siblingDefault;
|
|
266
|
-
return out;
|
|
267
|
-
};
|
|
268
|
-
return walk(root, new Set(), false);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function emitClone(node, access, depth) {
|
|
272
|
-
if (!node || typeof node !== 'object') return null;
|
|
273
|
-
if (depth > 12) return null;
|
|
274
|
-
// Only plain object nodes are cloneable, and only when the schema names
|
|
275
|
-
// every property it keeps. A $ref means the set of allowed keys is not
|
|
276
|
-
// this node's to decide. In-place applicators (allOf, anyOf, oneOf,
|
|
277
|
-
// if/then/else) are admitted under the same proof the unevaluated* error
|
|
278
|
-
// generators use: every property name they contribute must already be
|
|
279
|
-
// declared in this node's own `properties`, so they constrain values but
|
|
280
|
-
// never widen the key set. `not` never widens it. `unevaluatedProperties`
|
|
281
|
-
// is admitted only as `false` under that proof, where it is exactly
|
|
282
|
-
// `additionalProperties: false`.
|
|
283
|
-
if (node.$ref || node.patternProperties || node.additionalProperties === true) {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
// additionalProperties as a schema is the record shape (z.record and every
|
|
287
|
-
// generator's map type): the key set is open by declaration, so keeping
|
|
288
|
-
// every key is exact, and every undeclared value is rebuilt against that
|
|
289
|
-
// one schema. That is a proof, not a guess, so it is admitted. What stays
|
|
290
|
-
// out: applicators next to a record (a branch could constrain some keys'
|
|
291
|
-
// values beyond the record schema, and the rebuild would ignore it), and
|
|
292
|
-
// `additionalProperties: true`, where the value is unconstrained and this
|
|
293
|
-
// pass only copies what it can name. An open object with no
|
|
294
|
-
// `additionalProperties` at all also stays out, for the original reason:
|
|
295
|
-
// validate() accepts its unknown keys, so a parse() that strips them would
|
|
296
|
-
// silently drop allowed data, and one that keeps them raw would not be a
|
|
297
|
-
// sanitiser.
|
|
298
|
-
const apSchema = (node.additionalProperties && typeof node.additionalProperties === 'object')
|
|
299
|
-
? node.additionalProperties
|
|
300
|
-
: null;
|
|
301
|
-
if (apSchema) {
|
|
302
|
-
if (node.unevaluatedProperties !== undefined) return null;
|
|
303
|
-
if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
|
|
304
|
-
node.dependentSchemas !== undefined || node.dependencies !== undefined ||
|
|
305
|
-
node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return null;
|
|
306
|
-
}
|
|
307
|
-
if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== false) return null;
|
|
308
|
-
if (node.type !== 'object') return null;
|
|
309
|
-
if (!node.properties && !apSchema) return null;
|
|
310
|
-
const keys = node.properties ? Object.keys(node.properties) : [];
|
|
311
|
-
if (keys.length === 0 && !apSchema) return null;
|
|
312
|
-
if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
|
|
313
|
-
node.unevaluatedProperties === false) {
|
|
314
|
-
if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined ||
|
|
315
|
-
node.dependentSchemas !== undefined || node.dependencies !== undefined) return null;
|
|
316
|
-
const names = new Set();
|
|
317
|
-
const state = { prefix: 0 };
|
|
318
|
-
for (const k of ['allOf', 'anyOf', 'oneOf']) {
|
|
319
|
-
if (node[k] !== undefined) {
|
|
320
|
-
if (!Array.isArray(node[k])) return null;
|
|
321
|
-
for (const b of node[k]) if (!unevalContributions(b, 'props', names, state, 0)) return null;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
for (const k of ['if', 'then', 'else']) {
|
|
325
|
-
if (node[k] !== undefined && !unevalContributions(node[k], 'props', names, state, 0)) return null;
|
|
326
|
-
}
|
|
327
|
-
for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
|
|
328
|
-
}
|
|
329
|
-
const required = new Set(Array.isArray(node.required) ? node.required : []);
|
|
330
|
-
|
|
331
|
-
const fixed = [];
|
|
332
|
-
const conditional = [];
|
|
333
|
-
for (const key of keys) {
|
|
334
|
-
const prop = node.properties[key];
|
|
335
|
-
if (!prop || typeof prop !== 'object') return null;
|
|
336
|
-
if (prop.$ref) return null;
|
|
337
|
-
// A property that is required and carries a default is the one place the
|
|
338
|
-
// runtime and a raw-input validation disagree: the runtime fills the
|
|
339
|
-
// default before checking `required`, so `{}` validates there and would
|
|
340
|
-
// throw here. Declining keeps parse() exactly as strict as validate().
|
|
341
|
-
if (required.has(key) && prop.default !== undefined) return null;
|
|
342
|
-
// A default the property's own schema rejects would make parse() hand
|
|
343
|
-
// back a document validate() refuses: the runtime fills defaults before
|
|
344
|
-
// validating, so it catches the bad default, and a raw-input check here
|
|
345
|
-
// never sees it. Probe the default at emit time and decline when it does
|
|
346
|
-
// not hold, or when the subschema cannot be compiled to check it.
|
|
347
|
-
if (prop.default !== undefined) {
|
|
348
|
-
let probe = null;
|
|
349
|
-
try { probe = compileToJSCodegen(prop, null, null); } catch (_) { probe = null; }
|
|
350
|
-
if (!probe || !probe(prop.default)) return null;
|
|
351
|
-
}
|
|
352
|
-
const read = `${access}[${JSON.stringify(key)}]`;
|
|
353
|
-
const value = emitValueExpr(prop, read, depth);
|
|
354
|
-
if (value === null) return null;
|
|
355
|
-
// Computed key syntax: a plain `"__proto__":` in an object literal is
|
|
356
|
-
// the prototype slot, not a property; `["__proto__"]:` is a property.
|
|
357
|
-
if (required.has(key)) fixed.push(`[${JSON.stringify(key)}]: ${value}`);
|
|
358
|
-
else conditional.push({ key, value, dflt: prop.default });
|
|
359
|
-
}
|
|
360
|
-
if (fixed.length === 0 && conditional.length === 0 && !apSchema) return null;
|
|
361
|
-
|
|
362
|
-
const tmp = `_c${depth}`;
|
|
363
|
-
// The record loop: every key of the input that is not a declared property
|
|
364
|
-
// is kept, its value rebuilt against the additionalProperties schema. The
|
|
365
|
-
// input is already validated, so every such value satisfies that schema;
|
|
366
|
-
// the rebuild only strips inside it. Defaults under a record value are
|
|
367
|
-
// dropped before emission because the runtime's useDefaults does not fill
|
|
368
|
-
// them there, and parse() must equal validate().data, not improve on it.
|
|
369
|
-
let recordLoop = '';
|
|
370
|
-
if (apSchema) {
|
|
371
|
-
const kVar = `_k${depth}`;
|
|
372
|
-
const vVar = `_v${depth}`;
|
|
373
|
-
const valueExpr = emitValueExpr(stripDefaultsDeep(apSchema), vVar, depth);
|
|
374
|
-
if (valueExpr === null) return null;
|
|
375
|
-
let skip = '';
|
|
376
|
-
let declSet = '';
|
|
377
|
-
if (keys.length > 8) {
|
|
378
|
-
declSet = `const _d${depth} = new Set(${JSON.stringify(keys)}); `;
|
|
379
|
-
skip = `if (_d${depth}.has(${kVar})) continue; `;
|
|
380
|
-
} else if (keys.length > 0) {
|
|
381
|
-
skip = `if (${keys.map((k) => `${kVar} === ${JSON.stringify(k)}`).join(' || ')}) continue; `;
|
|
382
|
-
}
|
|
383
|
-
// The key comes from the input, so it can be the string "__proto__" (an
|
|
384
|
-
// own key only JSON.parse writes); assignment would hit the prototype
|
|
385
|
-
// setter, so that one key goes through defineProperty.
|
|
386
|
-
recordLoop = ` ${declSet}for (const ${kVar} of Object.keys(${access})) { ${skip}const ${vVar} = ${access}[${kVar}]; if (${kVar} === "__proto__") Object.defineProperty(${tmp}, ${kVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); else ${tmp}[${kVar}] = ${valueExpr}; }`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const literal = `{ ${fixed.join(', ')} }`;
|
|
390
|
-
if (conditional.length === 0 && !recordLoop) return literal;
|
|
391
|
-
// Optional properties are added only when present, so the result never
|
|
392
|
-
// gains a key the input did not have.
|
|
393
|
-
const adds = conditional
|
|
394
|
-
.map(({ key, value, dflt }) => {
|
|
395
|
-
// Object.hasOwn, not `in`: `in` also answers for what the instance
|
|
396
|
-
// inherits, so a property named `constructor` would copy the inherited
|
|
397
|
-
// function into the output that validate().data does not carry. And a
|
|
398
|
-
// key named `__proto__` cannot be written by assignment, which hits
|
|
399
|
-
// the prototype setter; defineProperty writes an own key.
|
|
400
|
-
const assign = key === '__proto__'
|
|
401
|
-
? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${value}, writable: true, enumerable: true, configurable: true });`
|
|
402
|
-
: `${tmp}[${JSON.stringify(key)}] = ${value};`;
|
|
403
|
-
const set = `if (Object.hasOwn(${access}, ${JSON.stringify(key)})) ${assign}`;
|
|
404
|
-
// The default is emitted as a literal inside the call, so an object or
|
|
405
|
-
// array default is a fresh value on every parse, never shared state.
|
|
406
|
-
if (dflt === undefined) return set;
|
|
407
|
-
const setDflt = key === '__proto__'
|
|
408
|
-
? `Object.defineProperty(${tmp}, ${JSON.stringify(key)}, { value: ${JSON.stringify(dflt)}, writable: true, enumerable: true, configurable: true });`
|
|
409
|
-
: `${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
|
|
410
|
-
return `${set} else ${setDflt}`;
|
|
411
|
-
})
|
|
412
|
-
.join(' ');
|
|
413
|
-
return `(function(){ const ${tmp} = ${literal}; ${adds}${recordLoop} return ${tmp}; })()`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// The value dispatch shared by declared properties and the record loop. A
|
|
417
|
-
// value is copyable only when the emitter can name everything that may live
|
|
418
|
-
// under it: a primitive holds nothing, an object with declared properties or
|
|
419
|
-
// a record schema is rebuilt the same way, an array of primitives has
|
|
420
|
-
// nowhere to hide a key. A $ref, a composition or an untyped node could all
|
|
421
|
-
// carry keys this code cannot see, and copying the reference would smuggle
|
|
422
|
-
// them into a value that claims to be sanitised, so the caller declines the
|
|
423
|
-
// whole clone instead.
|
|
424
|
-
// Drop every schema-position `default` in a subtree, leaving data positions
|
|
425
|
-
// (const, enum, examples) untouched. Used on a record's value schema, where
|
|
426
|
-
// the runtime's useDefaults fills nothing.
|
|
427
|
-
function stripDefaultsDeep(node) {
|
|
428
|
-
if (!node || typeof node !== 'object') return node;
|
|
429
|
-
if (Array.isArray(node)) return node.map(stripDefaultsDeep);
|
|
430
|
-
const out = {};
|
|
431
|
-
for (const k of Object.keys(node)) {
|
|
432
|
-
if (k === 'default') continue;
|
|
433
|
-
out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
|
|
434
|
-
}
|
|
435
|
-
return out;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
|
|
439
|
-
function emitValueExpr(prop, read, depth) {
|
|
440
|
-
if (!prop || typeof prop !== 'object') return null;
|
|
441
|
-
if (prop.$ref) return null;
|
|
442
|
-
const types = Array.isArray(prop.type) ? prop.type : [prop.type];
|
|
443
|
-
if (types.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
|
|
444
|
-
const isRecordable = (s) => s.properties ||
|
|
445
|
-
(s.additionalProperties && typeof s.additionalProperties === 'object');
|
|
446
|
-
if (prop.type === 'object' && isRecordable(prop)) {
|
|
447
|
-
return emitClone(prop, read, depth + 1);
|
|
448
|
-
}
|
|
449
|
-
if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
|
|
450
|
-
!Array.isArray(prop.items)) {
|
|
451
|
-
const it = prop.items;
|
|
452
|
-
const itemTypes = Array.isArray(it.type) ? it.type : [it.type];
|
|
453
|
-
if (itemTypes.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
|
|
454
|
-
if (it.type === 'object' && isRecordable(it)) {
|
|
455
|
-
const el = '_e' + depth;
|
|
456
|
-
const inner = emitClone(it, el, depth + 1);
|
|
457
|
-
if (!inner) return null;
|
|
458
|
-
return `${read}.map((${el}) => (${inner}))`;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
192
|
+
const { emitClone, inlineRefsForClone } = require('./clone-emit');
|
|
463
193
|
|
|
464
194
|
function toStandaloneModule(validator, opts) {
|
|
465
195
|
assertEmittable(validator, 'toStandaloneModule');
|