ata-validator 1.35.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/build.d.ts +23 -0
- package/build.mjs +4 -0
- package/index.d.ts +6 -0
- package/index.js +138 -47
- package/index.node.mjs +10 -0
- package/lib/aot-build.js +12 -6
- package/lib/compiled.js +17 -5
- package/lib/defaults.js +88 -0
- package/lib/js-compiler.js +211 -123
- package/lib/rejections.js +26 -6
- package/lib/schema-order.js +53 -3
- package/lib/validator-core.js +97 -118
- package/lib/version.js +1 -1
- package/package.json +16 -11
package/build.d.ts
CHANGED
|
@@ -155,3 +155,26 @@ export function schemaHash(schema: unknown): string;
|
|
|
155
155
|
* schema, plus `parse` when {@link ToStandaloneModuleOptions.parse} is set
|
|
156
156
|
* and `validateJSON` when {@link ToStandaloneModuleOptions.positions} is. */
|
|
157
157
|
export function toStandaloneModule(schema: unknown, options?: ToStandaloneModuleOptions): string | null;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether `new Validator(schema)` with default options can be replaced by
|
|
161
|
+
* `fromCompiled()` from `ata-validator/compiled`. False for a schema with
|
|
162
|
+
* custom `errorMessage`s. A caller also needs {@link compiledModuleFor} to
|
|
163
|
+
* return a module.
|
|
164
|
+
*/
|
|
165
|
+
export function compiledEligible(schema: unknown): boolean;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The schema a default `Validator` reads after normalization. Pass it to
|
|
169
|
+
* `fromCompiled()`, so defaults, error order and diagnostics follow the same
|
|
170
|
+
* document the runtime does.
|
|
171
|
+
*/
|
|
172
|
+
export function compiledSchemaFor(schema: unknown): object;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The module that replaces `new Validator(schema)`, or null where the
|
|
176
|
+
* replacement would not answer as the runtime does: a schema
|
|
177
|
+
* {@link compiledEligible} declines, one the emitter cannot compile, and one
|
|
178
|
+
* whose detailed errors the generator cannot produce.
|
|
179
|
+
*/
|
|
180
|
+
export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cjs' }): string | null;
|
package/build.mjs
CHANGED
|
@@ -8,4 +8,8 @@ export const watch = mod.watch;
|
|
|
8
8
|
export const bundleStandalone = mod.bundleStandalone;
|
|
9
9
|
export const bundleCompact = mod.bundleCompact;
|
|
10
10
|
export const toStandaloneModule = mod.toStandaloneModule;
|
|
11
|
+
export const schemaHash = mod.schemaHash;
|
|
12
|
+
export const compiledEligible = mod.compiledEligible;
|
|
13
|
+
export const compiledSchemaFor = mod.compiledSchemaFor;
|
|
14
|
+
export const compiledModuleFor = mod.compiledModuleFor;
|
|
11
15
|
export default mod;
|
package/index.d.ts
CHANGED
|
@@ -407,6 +407,12 @@ export interface ValidatorOptions {
|
|
|
407
407
|
* `'log'` warns through `logger` (or the console) and continues.
|
|
408
408
|
*/
|
|
409
409
|
strictSchema?: boolean | 'log';
|
|
410
|
+
/**
|
|
411
|
+
* The URI the schema was retrieved from. Relative references resolve against
|
|
412
|
+
* it when the schema declares no `$id` of its own, including a draft-07 root
|
|
413
|
+
* whose `$id` sits beside a `$ref` and is therefore ignored.
|
|
414
|
+
*/
|
|
415
|
+
baseURI?: string;
|
|
410
416
|
/** Receives `strictSchema: 'log'` warnings; `false` silences them. */
|
|
411
417
|
logger?: { warn(...args: unknown[]): void } | false;
|
|
412
418
|
/**
|
package/index.js
CHANGED
|
@@ -568,11 +568,117 @@ function emitRemovals(node, access, lines, depth, seen) {
|
|
|
568
568
|
else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
+
// The coercions the closure pass applies, emitted. Values: `number` and
|
|
572
|
+
// `integer` from numeric strings and booleans, `string` from numbers and
|
|
573
|
+
// booleans, `boolean` from "true"/"1" and "false"/"0".
|
|
574
|
+
const COERCIBLE_TYPES = new Set(['number', 'integer', 'string', 'boolean']);
|
|
575
|
+
function scalarCoercion(a, t) {
|
|
576
|
+
if (t === 'integer') return [`if(typeof ${a}==='string'){var _n=Number(${a});if(${a}!==''&&Number.isInteger(_n))${a}=_n}`, `if(typeof ${a}==='boolean')${a}=${a}?1:0`];
|
|
577
|
+
if (t === 'number') return [`if(typeof ${a}==='string'){var _n=Number(${a});if(${a}!==''&&!isNaN(_n))${a}=_n}`, `if(typeof ${a}==='boolean')${a}=${a}?1:0`];
|
|
578
|
+
if (t === 'string') return [`if(typeof ${a}==='number'||typeof ${a}==='boolean')${a}=String(${a})`];
|
|
579
|
+
return [`if(${a}==='true'||${a}==='1')${a}=true`, `if(${a}==='false'||${a}==='0')${a}=false`];
|
|
580
|
+
}
|
|
581
|
+
function isScalarCoercible(node) {
|
|
582
|
+
return !!(node && typeof node === 'object' && typeof node.type === 'string' && COERCIBLE_TYPES.has(node.type));
|
|
583
|
+
}
|
|
584
|
+
// Whether anything below `node` is coerced, as buildNodeCoercer decides it.
|
|
585
|
+
// Remembered per node for the build, since every enclosing node asks again.
|
|
586
|
+
function coercesInside(node, seen, memo) {
|
|
587
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return false;
|
|
588
|
+
if (memo && memo.has(node)) return memo.get(node);
|
|
589
|
+
seen.add(node);
|
|
590
|
+
let found = false;
|
|
591
|
+
if (node.properties) {
|
|
592
|
+
for (const [key, prop] of Object.entries(node.properties)) {
|
|
593
|
+
if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
|
|
594
|
+
if (isScalarCoercible(prop) || coercesInside(prop, seen, memo)) { found = true; break; }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (!found && node.items && typeof node.items === 'object' && !Array.isArray(node.items)) {
|
|
598
|
+
found = isScalarCoercible(node.items) || coercesInside(node.items, seen, memo);
|
|
599
|
+
}
|
|
600
|
+
seen.delete(node);
|
|
601
|
+
if (memo) memo.set(node, found);
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
function emitCoercions(node, ov, lines, st, depth) {
|
|
605
|
+
if (st.seen.has(node)) { st.cycle = true; return; }
|
|
606
|
+
st.seen.add(node);
|
|
607
|
+
if (node.properties) {
|
|
608
|
+
for (const [key, prop] of Object.entries(node.properties)) {
|
|
609
|
+
// Coercion writes with plain assignment, which for a key named
|
|
610
|
+
// __proto__ rewrites the prototype instead. The raw value still goes
|
|
611
|
+
// through validation, so skipping is a refusal to coerce, not a hole.
|
|
612
|
+
if (key === '__proto__' || !prop || typeof prop !== 'object') continue;
|
|
613
|
+
const k = JSON.stringify(key);
|
|
614
|
+
const a = `${ov}[${k}]`;
|
|
615
|
+
if (isScalarCoercible(prop)) lines.push(...scalarCoercion(a, prop.type));
|
|
616
|
+
// Wrapping a lone value in an array is a top-level rule only, as it was.
|
|
617
|
+
else if (depth === 0 && prop.type === 'array' && st.arrayMode) lines.push(`if(${k} in ${ov}&&${a}!==undefined&&!Array.isArray(${a}))${a}=[${a}]`);
|
|
618
|
+
// A leaf, the common case, has nothing below it to coerce.
|
|
619
|
+
if ((prop.properties || prop.items) && coercesInside(prop, new Set(), st.memo)) {
|
|
620
|
+
const n = '_c' + st.n++;
|
|
621
|
+
lines.push(`{const ${n}=${a};if(typeof ${n}==='object'&&${n}!==null){`);
|
|
622
|
+
emitCoercions(prop, n, lines, st, depth + 1);
|
|
623
|
+
lines.push('}}');
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const it = node.items;
|
|
628
|
+
if (it && typeof it === 'object' && !Array.isArray(it) && (isScalarCoercible(it) || coercesInside(it, new Set(), st.memo))) {
|
|
629
|
+
const i = '_i' + st.n++;
|
|
630
|
+
lines.push(`if(Array.isArray(${ov}))for(let ${i}=0;${i}<${ov}.length;${i}++){`);
|
|
631
|
+
if (isScalarCoercible(it)) lines.push(...scalarCoercion(`${ov}[${i}]`, it.type));
|
|
632
|
+
if (coercesInside(it, new Set(), st.memo)) {
|
|
633
|
+
const n = '_c' + st.n++;
|
|
634
|
+
lines.push(`{const ${n}=${ov}[${i}];if(typeof ${n}==='object'&&${n}!==null){`);
|
|
635
|
+
emitCoercions(it, n, lines, st, depth + 1);
|
|
636
|
+
lines.push('}}');
|
|
637
|
+
}
|
|
638
|
+
lines.push('}');
|
|
639
|
+
}
|
|
640
|
+
st.seen.delete(node);
|
|
641
|
+
}
|
|
642
|
+
function hasDefaultsInside(node, seen) {
|
|
643
|
+
if (!node || typeof node !== 'object' || !node.properties || seen.has(node)) return false;
|
|
644
|
+
seen.add(node);
|
|
645
|
+
let found = false;
|
|
646
|
+
for (const prop of Object.values(node.properties)) {
|
|
647
|
+
if (prop && typeof prop === 'object' && (prop.default !== undefined || hasDefaultsInside(prop, seen))) { found = true; break; }
|
|
648
|
+
}
|
|
649
|
+
seen.delete(node);
|
|
650
|
+
return found;
|
|
651
|
+
}
|
|
652
|
+
function emitDefaults(node, ov, lines, st) {
|
|
653
|
+
if (st.seen.has(node)) { st.cycle = true; return; }
|
|
654
|
+
st.seen.add(node);
|
|
655
|
+
for (const [key, prop] of Object.entries(node.properties || {})) {
|
|
656
|
+
if (!prop || typeof prop !== 'object') continue;
|
|
657
|
+
const k = JSON.stringify(key);
|
|
658
|
+
if (prop.default !== undefined) {
|
|
659
|
+
const def = JSON.stringify(prop.default);
|
|
660
|
+
// Assignment to a key named __proto__ hits the prototype setter
|
|
661
|
+
// instead of creating a property; defineProperty writes an own key.
|
|
662
|
+
lines.push(key === '__proto__'
|
|
663
|
+
? `if(!Object.hasOwn(${ov},${k}))Object.defineProperty(${ov},${k},{value:${def},writable:true,enumerable:true,configurable:true})`
|
|
664
|
+
: `if(!Object.hasOwn(${ov},${k}))${ov}[${k}]=${def}`);
|
|
665
|
+
}
|
|
666
|
+
// Into an own property that holds an object, arrays included, as the
|
|
667
|
+
// closure pass walks it.
|
|
668
|
+
if (prop.properties && hasDefaultsInside(prop, new Set())) {
|
|
669
|
+
const n = '_d' + st.n++;
|
|
670
|
+
lines.push(`if(Object.hasOwn(${ov},${k})){const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null){`);
|
|
671
|
+
emitDefaults(prop, n, lines, st);
|
|
672
|
+
lines.push('}}');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
st.seen.delete(node);
|
|
676
|
+
}
|
|
677
|
+
|
|
571
678
|
// Generate a fast preprocess function via codegen instead of closure arrays
|
|
572
679
|
function buildPreprocessCodegen(schema, options) {
|
|
573
680
|
if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
|
|
574
681
|
const lines = [];
|
|
575
|
-
const props = schema.properties;
|
|
576
682
|
|
|
577
683
|
// removeAdditional: strip unknown keys at every level the schema describes,
|
|
578
684
|
// not just the top one. The closure path below (collectRemovals) always
|
|
@@ -584,59 +690,44 @@ function buildPreprocessCodegen(schema, options) {
|
|
|
584
690
|
emitRemovals(schema, 'd', lines, 0, new Set());
|
|
585
691
|
}
|
|
586
692
|
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
lines.push(`if(typeof d[${k}]==='string'){var _n=Number(d[${k}]);if(d[${k}]!==''&&Number.isInteger(_n))d[${k}]=_n}`);
|
|
600
|
-
lines.push(`if(typeof d[${k}]==='boolean')d[${k}]=d[${k}]?1:0`);
|
|
601
|
-
} else if (t === 'number') {
|
|
602
|
-
lines.push(`if(typeof d[${k}]==='string'){var _n=Number(d[${k}]);if(d[${k}]!==''&&!isNaN(_n))d[${k}]=_n}`);
|
|
603
|
-
lines.push(`if(typeof d[${k}]==='boolean')d[${k}]=d[${k}]?1:0`);
|
|
604
|
-
} else if (t === 'string') {
|
|
605
|
-
lines.push(`if(typeof d[${k}]==='number'||typeof d[${k}]==='boolean')d[${k}]=String(d[${k}])`);
|
|
606
|
-
} else if (t === 'boolean') {
|
|
607
|
-
lines.push(`if(d[${k}]==='true'||d[${k}]==='1')d[${k}]=true`);
|
|
608
|
-
lines.push(`if(d[${k}]==='false'||d[${k}]==='0')d[${k}]=false`);
|
|
609
|
-
} else if (t === 'array' && options.coerceTypes === 'array') {
|
|
610
|
-
lines.push(`if(${k} in d&&d[${k}]!==undefined&&!Array.isArray(d[${k}]))d[${k}]=[d[${k}]]`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// defaults: inline per property
|
|
616
|
-
if (options.useDefaults !== false) {
|
|
617
|
-
for (const [key, prop] of Object.entries(props)) {
|
|
618
|
-
if (prop && typeof prop === 'object' && prop.default !== undefined) {
|
|
619
|
-
const k = JSON.stringify(key);
|
|
620
|
-
const def = JSON.stringify(prop.default);
|
|
621
|
-
// Assignment to a key named __proto__ hits the prototype setter
|
|
622
|
-
// instead of creating a property; defineProperty writes an own key.
|
|
623
|
-
lines.push(key === '__proto__'
|
|
624
|
-
? `if(!Object.hasOwn(d,${k}))Object.defineProperty(d,${k},{value:${def},writable:true,enumerable:true,configurable:true})`
|
|
625
|
-
: `if(!Object.hasOwn(d,${k}))d[${k}]=${def}`);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
693
|
+
// Coercion and defaults reach every depth the closure passes in
|
|
694
|
+
// validator-core.js reach, with the same rules in the same order, so the
|
|
695
|
+
// interpreted engine, which uses those passes, gives the same answer. Both
|
|
696
|
+
// used to stop at the top-level properties. A schema object that contains
|
|
697
|
+
// itself declines here, and the closure passes, which carry a guard for it,
|
|
698
|
+
// take the schema.
|
|
699
|
+
const st = { n: 0, seen: new Set(), cycle: false, arrayMode: options.coerceTypes === 'array', memo: new Map() };
|
|
700
|
+
if (options.coerceTypes) emitCoercions(schema, 'd', lines, st, 0);
|
|
701
|
+
// hasDefaultsInside answers without emitting; most schemas have no default,
|
|
702
|
+
// and the emitting walk allocates for every property it visits.
|
|
703
|
+
if (options.useDefaults !== false && hasDefaultsInside(schema, new Set())) emitDefaults(schema, 'd', lines, st);
|
|
704
|
+
if (st.cycle) return null;
|
|
629
705
|
|
|
630
706
|
if (lines.length === 0) return null;
|
|
631
707
|
// Data may legitimately be null or a non-object (e.g. a `['object','null']`
|
|
632
708
|
// schema), so the per-property mutations must not run on it.
|
|
633
709
|
lines.unshift(`if(d===null||typeof d!=='object')return`);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
710
|
+
// The pass depends on property names, types, defaults and which objects
|
|
711
|
+
// are closed, not on the constraints the verdict checks, so routes that
|
|
712
|
+
// take the same shape with different limits (a page/limit querystring, an
|
|
713
|
+
// id param) emit the same source. Compiling it is most of what building the
|
|
714
|
+
// pass costs, so the function is kept by its source and compiled once. It
|
|
715
|
+
// holds no state: it rewrites the object it is given and nothing else.
|
|
716
|
+
const src = lines.join('\n');
|
|
717
|
+
let fn = _preprocessBySource.get(src);
|
|
718
|
+
if (fn === undefined) {
|
|
719
|
+
try {
|
|
720
|
+
fn = new Function('d', src);
|
|
721
|
+
} catch {
|
|
722
|
+
fn = null;
|
|
723
|
+
}
|
|
724
|
+
if (_preprocessBySource.size >= PREPROCESS_SOURCE_LIMIT) _preprocessBySource.clear();
|
|
725
|
+
_preprocessBySource.set(src, fn);
|
|
638
726
|
}
|
|
727
|
+
return fn;
|
|
639
728
|
}
|
|
729
|
+
const _preprocessBySource = new Map();
|
|
730
|
+
const PREPROCESS_SOURCE_LIMIT = 4096;
|
|
640
731
|
|
|
641
732
|
|
|
642
733
|
// parse() for the full package: a generated function that copies the keys the
|
package/index.node.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The ESM entry Node itself resolves (the `module-sync` condition, Node 22.10
|
|
2
|
+
// and later). Importing index.js with an `import` statement makes Node run
|
|
3
|
+
// its CommonJS lexer over every module the package requires, to find named
|
|
4
|
+
// exports this file lists by hand anyway: 7.7 ms to load against 5.5 through
|
|
5
|
+
// require. Bundlers do not match `module-sync` and keep index.mjs, whose
|
|
6
|
+
// static import they can follow.
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
const mod = createRequire(import.meta.url)('./index.js');
|
|
9
|
+
export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
|
|
10
|
+
export default mod;
|
package/lib/aot-build.js
CHANGED
|
@@ -292,17 +292,22 @@ const { schemaHash } = require('./schema-hash');
|
|
|
292
292
|
// fromCompiled() around the module toStandaloneModule(schema) writes, which a
|
|
293
293
|
// bundler plugin does so the runtime compiler stays out of the bundle. The
|
|
294
294
|
// wrapper reproduces a default Validator's results exactly for these schemas;
|
|
295
|
-
// tests/test_compiled_parity.js holds that. Declined:
|
|
296
|
-
//
|
|
297
|
-
// module applies no defaults, and custom error messages, which the core
|
|
298
|
-
// applies in a layer the wrapper does not carry. A caller must also get a
|
|
295
|
+
// tests/test_compiled_parity.js holds that. Declined: custom error messages,
|
|
296
|
+
// which the core applies in a layer the wrapper does not carry. A caller must also get a
|
|
299
297
|
// module back from toStandaloneModule, which returns null for a schema it
|
|
300
298
|
// cannot compile. The text checks are deliberately coarse: a property named
|
|
301
299
|
// `default` declines too, and declining costs only the saving.
|
|
302
300
|
function compiledEligible(schema) {
|
|
303
301
|
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return false;
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
return !JSON.stringify(schema).includes('"errorMessage"');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The schema a default Validator reads, after normalization: what
|
|
306
|
+
// fromCompiled() takes, so its defaults, sorting and diagnostics follow the
|
|
307
|
+
// same document the runtime does.
|
|
308
|
+
function compiledSchemaFor(schema) {
|
|
309
|
+
const { Validator } = require('../index.js');
|
|
310
|
+
return new Validator(schema)._schemaObj;
|
|
306
311
|
}
|
|
307
312
|
|
|
308
313
|
// The module that replaces `new Validator(schema)`, or null where the
|
|
@@ -328,6 +333,7 @@ function compiledModuleFor(schema, opts) {
|
|
|
328
333
|
|
|
329
334
|
module.exports = {
|
|
330
335
|
compiledEligible,
|
|
336
|
+
compiledSchemaFor,
|
|
331
337
|
compiledModuleFor,
|
|
332
338
|
build,
|
|
333
339
|
expandGlobs,
|
package/lib/compiled.js
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
// objects, which come from the same rejection classes the validator core uses.
|
|
9
9
|
// tests/test_compiled_parity.js holds that over the official suite.
|
|
10
10
|
//
|
|
11
|
-
// Only for a schema compiledEligible() accepts
|
|
12
|
-
// (
|
|
13
|
-
// than the defaults stay on the
|
|
11
|
+
// Only for a schema compiledEligible() accepts, and with the schema
|
|
12
|
+
// compiledSchemaFor() returns: the one the runtime reads, after normalization.
|
|
13
|
+
// Custom error messages and every option other than the defaults stay on the
|
|
14
|
+
// runtime. Defaults are applied as a default Validator applies them, before
|
|
15
|
+
// any check, with the same code.
|
|
14
16
|
|
|
15
17
|
const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy } = require('./rejections');
|
|
18
|
+
const { buildDefaultsApplier } = require('./defaults');
|
|
16
19
|
|
|
17
20
|
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
18
21
|
const EMPTY_ERRORS = Object.freeze([]);
|
|
@@ -41,9 +44,16 @@ class CompiledState {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function fromCompiled(mod, schema) {
|
|
44
|
-
const isValid = mod.isValid;
|
|
45
|
-
const errFn = mod.validate;
|
|
46
47
|
const self = new CompiledState(schema);
|
|
48
|
+
const fill = buildDefaultsApplier(schema);
|
|
49
|
+
if (fill) {
|
|
50
|
+
self._mutatesInput = true;
|
|
51
|
+
self._preprocess = fill;
|
|
52
|
+
}
|
|
53
|
+
// With defaults, the document is filled in before it is checked, on every
|
|
54
|
+
// entry point, as the runtime does.
|
|
55
|
+
const isValid = fill ? (d) => { fill(d); return mod.isValid(d); } : mod.isValid;
|
|
56
|
+
const errFn = mod.validate;
|
|
47
57
|
// The generated function's own result, rejected: if it says valid where the
|
|
48
58
|
// verdict said no, the disagreement is reported rather than turned into an
|
|
49
59
|
// acceptance, as the core does.
|
|
@@ -63,6 +73,8 @@ function fromCompiled(mod, schema) {
|
|
|
63
73
|
return {
|
|
64
74
|
validate(data) {
|
|
65
75
|
if (isValid(data)) return { valid: true, data, errors: EMPTY_ERRORS };
|
|
76
|
+
// The runtime returns the rich rejection directly when it rewrites input.
|
|
77
|
+
if (fill) return rich(data);
|
|
66
78
|
return new LazyRejection(buildErrors, data, buildRawErrors);
|
|
67
79
|
},
|
|
68
80
|
isValidObject(data) {
|
package/lib/defaults.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The closure pass that fills `default` values, shared by the validator core
|
|
4
|
+
// and by the wrapper around a compiled module, so both fill defaults the same
|
|
5
|
+
// way. The generated pass in index.js is held to this one by
|
|
6
|
+
// tests/test_nested_defaults_engines.js.
|
|
7
|
+
|
|
8
|
+
// Extract default values from a schema tree. Returns a function that applies
|
|
9
|
+
// defaults to an object in-place (mutates), or null if no defaults exist.
|
|
10
|
+
function buildDefaultsApplier(schema) {
|
|
11
|
+
if (typeof schema !== "object" || schema === null) return null;
|
|
12
|
+
const actions = [];
|
|
13
|
+
collectDefaults(schema, actions);
|
|
14
|
+
if (actions.length === 0) return null;
|
|
15
|
+
return (data) => {
|
|
16
|
+
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Write an own property. Plain assignment of a key named `__proto__` does
|
|
21
|
+
// not create a property at all: it hits the Object.prototype setter and
|
|
22
|
+
// rewrites the object's prototype, which is how a schema could reach
|
|
23
|
+
// Object.prototype itself. defineProperty has no such special case.
|
|
24
|
+
function setOwn(obj, key, val) {
|
|
25
|
+
if (key === "__proto__") {
|
|
26
|
+
Object.defineProperty(obj, key, {
|
|
27
|
+
value: val,
|
|
28
|
+
writable: true,
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
});
|
|
32
|
+
} else {
|
|
33
|
+
obj[key] = val;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectDefaults(schema, actions, path) {
|
|
38
|
+
if (typeof schema !== "object" || schema === null) return;
|
|
39
|
+
const props = schema.properties;
|
|
40
|
+
if (!props) return;
|
|
41
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
42
|
+
if (prop && typeof prop === "object" && prop.default !== undefined) {
|
|
43
|
+
const defaultVal = prop.default;
|
|
44
|
+
if (!path) {
|
|
45
|
+
actions.push((data) => {
|
|
46
|
+
if (typeof data === "object" && data !== null && !Object.hasOwn(data, key)) {
|
|
47
|
+
setOwn(data,
|
|
48
|
+
key,
|
|
49
|
+
typeof defaultVal === "object" && defaultVal !== null
|
|
50
|
+
? JSON.parse(JSON.stringify(defaultVal))
|
|
51
|
+
: defaultVal);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
const parentPath = path;
|
|
56
|
+
actions.push((data) => {
|
|
57
|
+
let target = data;
|
|
58
|
+
for (let j = 0; j < parentPath.length; j++) {
|
|
59
|
+
if (typeof target !== "object" || target === null) return;
|
|
60
|
+
// Own keys only. `target[key]` for an inherited name walks the
|
|
61
|
+
// prototype chain: a parent named `__proto__` that the instance
|
|
62
|
+
// does not carry resolved to Object.prototype, and the child
|
|
63
|
+
// defaults were written onto it, for every object in the realm.
|
|
64
|
+
if (!Object.hasOwn(target, parentPath[j])) return;
|
|
65
|
+
target = target[parentPath[j]];
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
typeof target === "object" &&
|
|
69
|
+
target !== null &&
|
|
70
|
+
!Object.hasOwn(target, key)
|
|
71
|
+
) {
|
|
72
|
+
setOwn(target,
|
|
73
|
+
key,
|
|
74
|
+
typeof defaultVal === "object" && defaultVal !== null
|
|
75
|
+
? JSON.parse(JSON.stringify(defaultVal))
|
|
76
|
+
: defaultVal);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Recurse into nested object schemas
|
|
82
|
+
if (prop && typeof prop === "object" && prop.properties) {
|
|
83
|
+
collectDefaults(prop, actions, (path || []).concat(key));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { buildDefaultsApplier, setOwn };
|