ascertain 3.2.21 → 3.2.22
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 +49 -29
- package/build/index.cjs +439 -74
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +7 -8
- package/build/index.js +439 -74
- package/build/index.js.map +1 -1
- package/package.json +6 -4
- package/src/index.ts +439 -81
- package/build/__bench__/benchmark.d.ts +0 -1
package/src/index.ts
CHANGED
|
@@ -19,6 +19,16 @@ const OPTIONAL = Symbol.for('@@optional');
|
|
|
19
19
|
const TUPLE = Symbol.for('@@tuple');
|
|
20
20
|
const DISCRIMINATED = Symbol.for('@@discriminated');
|
|
21
21
|
const CHECK = Symbol.for('@@check');
|
|
22
|
+
const ERR = Symbol.for('@@err');
|
|
23
|
+
|
|
24
|
+
interface ErrShape {
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly [ERR]: true;
|
|
27
|
+
}
|
|
28
|
+
function ErrCtor(this: ErrShape, message: string): void {
|
|
29
|
+
(this as { message: string }).message = message;
|
|
30
|
+
}
|
|
31
|
+
(ErrCtor.prototype as { [ERR]: true })[ERR] = true;
|
|
22
32
|
|
|
23
33
|
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
24
34
|
|
|
@@ -304,9 +314,43 @@ export const oneOf = <T extends EnumLike>(values: (string | number)[] | T, messa
|
|
|
304
314
|
* @param value - The base64-encoded string to decode.
|
|
305
315
|
* @returns The decoded UTF-8 string.
|
|
306
316
|
*/
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
317
|
+
const utf8Encoder = new TextEncoder();
|
|
318
|
+
const utf8Decoder = new TextDecoder('utf-8');
|
|
319
|
+
|
|
320
|
+
const fromBase64Bytes = (value: string): Uint8Array => {
|
|
321
|
+
const bin = atob(value);
|
|
322
|
+
const out = new Uint8Array(bin.length);
|
|
323
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
324
|
+
return out;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const fromBase64 = (value: string): string => utf8Decoder.decode(fromBase64Bytes(value));
|
|
328
|
+
|
|
329
|
+
const HEX_LUT = (() => {
|
|
330
|
+
const t = new Int8Array(256).fill(-1);
|
|
331
|
+
for (let i = 0; i < 10; i++) t[48 + i] = i;
|
|
332
|
+
for (let i = 0; i < 6; i++) {
|
|
333
|
+
t[97 + i] = 10 + i;
|
|
334
|
+
t[65 + i] = 10 + i;
|
|
335
|
+
}
|
|
336
|
+
return t;
|
|
337
|
+
})();
|
|
338
|
+
|
|
339
|
+
const fromHex = (value: string): Uint8Array => {
|
|
340
|
+
let start = 0;
|
|
341
|
+
if (value.length >= 2 && value.charCodeAt(0) === 48 && (value.charCodeAt(1) | 32) === 120) {
|
|
342
|
+
start = 2;
|
|
343
|
+
}
|
|
344
|
+
const digits = value.length - start;
|
|
345
|
+
if (digits === 0 || digits % 2 !== 0) throw new TypeError('invalid hex length');
|
|
346
|
+
const out = new Uint8Array(digits / 2);
|
|
347
|
+
for (let i = 0; i < out.length; i++) {
|
|
348
|
+
const byte = (HEX_LUT[value.charCodeAt(start + i * 2)] << 4) | HEX_LUT[value.charCodeAt(start + i * 2 + 1)];
|
|
349
|
+
if (byte < 0) throw new TypeError('invalid hex digit');
|
|
350
|
+
out[i] = byte;
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
};
|
|
310
354
|
|
|
311
355
|
const MULTIPLIERS = {
|
|
312
356
|
ms: 1,
|
|
@@ -329,7 +373,7 @@ const TIME_REGEX = /^(\d*\.?\d*)(ms|s|m|h|d|w)?$/;
|
|
|
329
373
|
* @param message - The error message.
|
|
330
374
|
* @returns A TypeError instance typed as T.
|
|
331
375
|
*/
|
|
332
|
-
export const asError = <T>(message: string) => new
|
|
376
|
+
export const asError = <T>(message: string) => new (ErrCtor as unknown as new (m: string) => { message: string; [ERR]: true })(message) as unknown as T;
|
|
333
377
|
|
|
334
378
|
/**
|
|
335
379
|
* Type casting utilities for parsing strings into typed values.
|
|
@@ -456,6 +500,17 @@ export const as = {
|
|
|
456
500
|
return asError(`Invalid value ${value}, expected a valid base64 string`);
|
|
457
501
|
}
|
|
458
502
|
},
|
|
503
|
+
|
|
504
|
+
data: (value: string | undefined, type: 'utf-8' | 'hex' | 'base64' = 'utf-8'): Uint8Array => {
|
|
505
|
+
if (typeof value !== 'string') return asError(`Invalid value ${value}, expected a string`);
|
|
506
|
+
try {
|
|
507
|
+
if (type === 'hex') return fromHex(value);
|
|
508
|
+
if (type === 'base64') return fromBase64Bytes(value);
|
|
509
|
+
return utf8Encoder.encode(value);
|
|
510
|
+
} catch {
|
|
511
|
+
return asError(`Invalid value ${value}, expected a valid ${type} string`);
|
|
512
|
+
}
|
|
513
|
+
},
|
|
459
514
|
};
|
|
460
515
|
|
|
461
516
|
const DATETIME_RE = /^\d{4}-[01]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d{2}(?::?\d{2})?)$/i;
|
|
@@ -553,6 +608,7 @@ class Context {
|
|
|
553
608
|
public readonly registry: unknown[] = [];
|
|
554
609
|
private readonly lookupMap: Map<unknown, number> = new Map();
|
|
555
610
|
private varIndex = 0;
|
|
611
|
+
public pure = false;
|
|
556
612
|
|
|
557
613
|
register(value: unknown): number {
|
|
558
614
|
const index = this.lookupMap.get(value);
|
|
@@ -572,35 +628,148 @@ class Context {
|
|
|
572
628
|
}
|
|
573
629
|
|
|
574
630
|
type Mode =
|
|
575
|
-
| { fast: true; onFail?: string }
|
|
576
|
-
| { fast: false; firstError: true; issues: string; path: PropertyKey[]; pathExpr: string }
|
|
577
|
-
| {
|
|
631
|
+
| { fast: true; onFail?: string; indexed?: boolean }
|
|
632
|
+
| { fast: false; firstError: true; issues: string; path: PropertyKey[]; pathExpr: string; startVar?: string; dynamicParts?: string[] }
|
|
633
|
+
| {
|
|
634
|
+
fast: false;
|
|
635
|
+
firstError: false;
|
|
636
|
+
issues: string;
|
|
637
|
+
path: PropertyKey[];
|
|
638
|
+
pathExpr: string;
|
|
639
|
+
startVar?: string;
|
|
640
|
+
issuesReady?: boolean;
|
|
641
|
+
dynamicParts?: string[];
|
|
642
|
+
};
|
|
578
643
|
|
|
579
644
|
const isTagged = (schema: unknown): schema is Tagged<unknown> => (schema as Tagged<unknown>)?.[$op] !== undefined;
|
|
580
645
|
|
|
581
|
-
const
|
|
646
|
+
const buildDynamicPathExpr = (staticPath: PropertyKey[], dynamicParts: string[]): string =>
|
|
647
|
+
`[${[...staticPath.map((k) => JSON.stringify(k)), ...dynamicParts].join(',')}]`;
|
|
648
|
+
|
|
649
|
+
const childMode = (mode: Exclude<Mode, { fast: true }>, key: PropertyKey | { dynamic: string }, ctx: Context): Mode => {
|
|
650
|
+
const carryReady = !mode.firstError && mode.issuesReady;
|
|
582
651
|
if (typeof key === 'object' && 'dynamic' in key) {
|
|
583
|
-
|
|
652
|
+
const dynamicParts = [...(mode.dynamicParts ?? []), key.dynamic];
|
|
653
|
+
const m: Mode = {
|
|
654
|
+
fast: false,
|
|
655
|
+
firstError: mode.firstError,
|
|
656
|
+
issues: mode.issues,
|
|
657
|
+
path: mode.path,
|
|
658
|
+
pathExpr: buildDynamicPathExpr(mode.path, dynamicParts),
|
|
659
|
+
dynamicParts,
|
|
660
|
+
};
|
|
661
|
+
if (carryReady) (m as { issuesReady: boolean }).issuesReady = true;
|
|
662
|
+
return m;
|
|
663
|
+
}
|
|
664
|
+
if (mode.dynamicParts) {
|
|
665
|
+
const dynamicParts = [...mode.dynamicParts, JSON.stringify(key)];
|
|
666
|
+
const m: Mode = {
|
|
584
667
|
fast: false,
|
|
585
668
|
firstError: mode.firstError,
|
|
586
669
|
issues: mode.issues,
|
|
587
670
|
path: mode.path,
|
|
588
|
-
pathExpr:
|
|
671
|
+
pathExpr: buildDynamicPathExpr(mode.path, dynamicParts),
|
|
672
|
+
dynamicParts,
|
|
589
673
|
};
|
|
674
|
+
if (carryReady) (m as { issuesReady: boolean }).issuesReady = true;
|
|
675
|
+
return m;
|
|
590
676
|
}
|
|
591
677
|
const newPath = [...mode.path, key];
|
|
592
|
-
|
|
678
|
+
const pathExpr = `reg[${ctx.register(Object.freeze(newPath))}]`;
|
|
679
|
+
const m: Mode = { fast: false, firstError: mode.firstError, issues: mode.issues, path: newPath, pathExpr };
|
|
680
|
+
if (carryReady) (m as { issuesReady: boolean }).issuesReady = true;
|
|
681
|
+
return m;
|
|
593
682
|
};
|
|
594
683
|
|
|
595
684
|
const toLiteral = (value: unknown): string => (typeof value === 'bigint' ? `${value}n` : JSON.stringify(value));
|
|
596
685
|
|
|
686
|
+
const collapsibleTypeOf = (s: unknown): string | null =>
|
|
687
|
+
s === String
|
|
688
|
+
? 'string'
|
|
689
|
+
: s === Number
|
|
690
|
+
? 'number'
|
|
691
|
+
: s === Boolean
|
|
692
|
+
? 'boolean'
|
|
693
|
+
: s === BigInt
|
|
694
|
+
? 'bigint'
|
|
695
|
+
: s === Symbol
|
|
696
|
+
? 'symbol'
|
|
697
|
+
: typeof s === 'function' && (s as { name?: string })?.name === 'Function'
|
|
698
|
+
? 'function'
|
|
699
|
+
: null;
|
|
700
|
+
|
|
701
|
+
const buildCollapsedOr = (schemas: unknown[], v: string): string => {
|
|
702
|
+
let hasNull = false;
|
|
703
|
+
let hasUndefined = false;
|
|
704
|
+
const groups = new Map<string, { ctor: boolean; literals: unknown[] }>();
|
|
705
|
+
for (const s of schemas) {
|
|
706
|
+
if (s === null) {
|
|
707
|
+
hasNull = true;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (s === undefined) {
|
|
711
|
+
hasUndefined = true;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const ct = collapsibleTypeOf(s);
|
|
715
|
+
if (ct) {
|
|
716
|
+
let g = groups.get(ct);
|
|
717
|
+
if (!g) {
|
|
718
|
+
g = { ctor: false, literals: [] };
|
|
719
|
+
groups.set(ct, g);
|
|
720
|
+
}
|
|
721
|
+
g.ctor = true;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const t = typeof s;
|
|
725
|
+
let g = groups.get(t);
|
|
726
|
+
if (!g) {
|
|
727
|
+
g = { ctor: false, literals: [] };
|
|
728
|
+
groups.set(t, g);
|
|
729
|
+
}
|
|
730
|
+
g.literals.push(s);
|
|
731
|
+
}
|
|
732
|
+
const clauses: string[] = [];
|
|
733
|
+
if (hasNull) clauses.push(`${v} === null`);
|
|
734
|
+
if (hasUndefined) clauses.push(`${v} === undefined`);
|
|
735
|
+
for (const [type, { ctor, literals }] of groups) {
|
|
736
|
+
if (ctor) {
|
|
737
|
+
clauses.push(type === 'number' ? `(typeof ${v} === 'number' && ${v} === ${v})` : `typeof ${v} === '${type}'`);
|
|
738
|
+
} else if (literals.length === 1) {
|
|
739
|
+
clauses.push(`${v} === ${toLiteral(literals[0])}`);
|
|
740
|
+
} else {
|
|
741
|
+
clauses.push(`(typeof ${v} === '${type}' && (${literals.map((l) => `${v} === ${toLiteral(l)}`).join(' || ')}))`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return clauses.join(' || ');
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const buildExpectedDesc = (schemas: unknown[]): string => {
|
|
748
|
+
const parts: string[] = [];
|
|
749
|
+
for (const s of schemas) {
|
|
750
|
+
if (s === null) parts.push('null');
|
|
751
|
+
else if (s === undefined) parts.push('undefined');
|
|
752
|
+
else if (collapsibleTypeOf(s) !== null) parts.push((s as { name: string }).name);
|
|
753
|
+
else parts.push(toLiteral(s) as string);
|
|
754
|
+
}
|
|
755
|
+
return parts.join(', ');
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const isCollapsible = (s: unknown): boolean =>
|
|
759
|
+
s === null || s === undefined || (typeof s !== 'object' && typeof s !== 'function' && typeof s !== 'symbol') || collapsibleTypeOf(s) !== null;
|
|
760
|
+
|
|
597
761
|
const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode: Mode): string => {
|
|
598
762
|
const emit = mode.fast
|
|
599
763
|
? null
|
|
600
764
|
: mode.firstError
|
|
601
765
|
? (msg: string) => `${mode.issues} = [{ message: ${msg}, path: ${mode.pathExpr} }]; return ${mode.issues};`
|
|
602
|
-
:
|
|
766
|
+
: !mode.firstError && mode.issuesReady
|
|
767
|
+
? (msg: string) => `${mode.issues}.push({ message: ${msg}, path: ${mode.pathExpr} });`
|
|
768
|
+
: (msg: string) => `(${mode.issues} || (${mode.issues} = [])).push({ message: ${msg}, path: ${mode.pathExpr} });`;
|
|
603
769
|
const fail = mode.fast ? (mode.onFail ?? 'return false;') : '';
|
|
770
|
+
const errChk = (v: string) => (context.pure ? '' : ` || (typeof ${v} === 'object' && ${v} !== null && ${v}[err] === true)`);
|
|
771
|
+
const errBranch = (v: string) =>
|
|
772
|
+
context.pure ? '' : `else if (typeof ${v} === 'object' && ${v} !== null && ${v}[err] === true) { ${emit!(`\`\${${v}.message}\``)} }`;
|
|
604
773
|
|
|
605
774
|
if (isTagged(schema)) {
|
|
606
775
|
const tag = schema[$op];
|
|
@@ -612,6 +781,24 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
612
781
|
const valueAlias = context.unique('v');
|
|
613
782
|
const foundValid = context.unique('valid');
|
|
614
783
|
if (mode.fast) {
|
|
784
|
+
const collapsible: unknown[] = [];
|
|
785
|
+
const complex: Schema<T>[] = [];
|
|
786
|
+
for (const s of schema.schemas) {
|
|
787
|
+
if (isCollapsible(s)) collapsible.push(s);
|
|
788
|
+
else complex.push(s as Schema<T>);
|
|
789
|
+
}
|
|
790
|
+
if (collapsible.length > 0 && complex.length === 0) {
|
|
791
|
+
return `const ${valueAlias} = ${valuePath};\nif (!(${buildCollapsedOr(collapsible, valueAlias)})) { ${fail} }`;
|
|
792
|
+
}
|
|
793
|
+
if (collapsible.length > 0) {
|
|
794
|
+
const condition = buildCollapsedOr(collapsible, valueAlias);
|
|
795
|
+
const branches = complex.map((s) => {
|
|
796
|
+
const branchValid = context.unique('valid');
|
|
797
|
+
const branchCode = codeGen(s, context, valueAlias, { ...mode, onFail: `${branchValid} = false;` });
|
|
798
|
+
return `if (!${foundValid}) { let ${branchValid} = true; ${branchCode} if (${branchValid}) { ${foundValid} = true; } }`;
|
|
799
|
+
});
|
|
800
|
+
return `const ${valueAlias} = ${valuePath};\nlet ${foundValid} = ${condition};\n${branches.join('\n')}\nif (!${foundValid}) { ${fail} }`;
|
|
801
|
+
}
|
|
615
802
|
const branches = schema.schemas.map((s) => {
|
|
616
803
|
const branchValid = context.unique('valid');
|
|
617
804
|
const branchCode = codeGen(s, context, valueAlias, { ...mode, onFail: `${branchValid} = false;` });
|
|
@@ -619,6 +806,11 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
619
806
|
});
|
|
620
807
|
return `const ${valueAlias} = ${valuePath};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { ${fail} }`;
|
|
621
808
|
} else if (mode.firstError) {
|
|
809
|
+
if (schema.schemas.every(isCollapsible)) {
|
|
810
|
+
const condition = buildCollapsedOr(schema.schemas, valueAlias);
|
|
811
|
+
const expected = buildExpectedDesc(schema.schemas);
|
|
812
|
+
return `const ${valueAlias} = ${valuePath};\nif (!(${condition})) { ${mode.issues} = [{ message: \`Invalid value \${${valueAlias}}, expected one of: ${expected}\`, path: ${mode.pathExpr} }]; return ${mode.issues}; }`;
|
|
813
|
+
}
|
|
622
814
|
const firstBranchIssues = context.unique('iss');
|
|
623
815
|
const branches = schema.schemas.map((s, idx) => {
|
|
624
816
|
const branchIssues = context.unique('iss');
|
|
@@ -636,6 +828,14 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
636
828
|
});
|
|
637
829
|
return `const ${valueAlias} = ${valuePath};\nlet ${firstBranchIssues};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { return ${firstBranchIssues}; }`;
|
|
638
830
|
} else {
|
|
831
|
+
if (schema.schemas.every(isCollapsible)) {
|
|
832
|
+
const condition = buildCollapsedOr(schema.schemas, valueAlias);
|
|
833
|
+
const expected = buildExpectedDesc(schema.schemas);
|
|
834
|
+
const push = mode.issuesReady
|
|
835
|
+
? `${mode.issues}.push({ message: \`Invalid value \${${valueAlias}}, expected one of: ${expected}\`, path: ${mode.pathExpr} });`
|
|
836
|
+
: `(${mode.issues} || (${mode.issues} = [])).push({ message: \`Invalid value \${${valueAlias}}, expected one of: ${expected}\`, path: ${mode.pathExpr} });`;
|
|
837
|
+
return `const ${valueAlias} = ${valuePath};\nif (!(${condition})) { ${push} }`;
|
|
838
|
+
}
|
|
639
839
|
const localIssues = context.unique('iss');
|
|
640
840
|
const branches = schema.schemas.map((s) => {
|
|
641
841
|
const branchIssues = context.unique('iss');
|
|
@@ -648,11 +848,33 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
648
848
|
});
|
|
649
849
|
return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } else { ${localIssues}.push(...${branchIssues}); } }`;
|
|
650
850
|
});
|
|
651
|
-
|
|
851
|
+
const pushExpr =
|
|
852
|
+
!mode.fast && !mode.firstError && mode.issuesReady
|
|
853
|
+
? `${mode.issues}.push(...${localIssues})`
|
|
854
|
+
: `(${mode.issues} || (${mode.issues} = [])).push(...${localIssues})`;
|
|
855
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${localIssues} = [];\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { ${pushExpr}; }`;
|
|
652
856
|
}
|
|
653
857
|
} else if (tag === OPTIONAL) {
|
|
654
858
|
const valueAlias = context.unique('v');
|
|
655
|
-
|
|
859
|
+
const inner = (schema as OptionalShape<T>).schemas[0];
|
|
860
|
+
if (!mode.fast && typeof inner === 'function') {
|
|
861
|
+
const iname = (inner as { name?: string })?.name;
|
|
862
|
+
const is = inner as unknown;
|
|
863
|
+
const pt =
|
|
864
|
+
is === String ? 'string' : is === Number ? 'number' : is === Boolean ? 'boolean' : is === BigInt ? 'bigint' : is === Symbol ? 'symbol' : null;
|
|
865
|
+
if (pt) {
|
|
866
|
+
const typeMsgs = Object.fromEntries(
|
|
867
|
+
['string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'object', 'function'].map((t) => [t, `Invalid type ${t}, expected type ${iname}`]),
|
|
868
|
+
);
|
|
869
|
+
const typeMsgIdx = context.register(typeMsgs);
|
|
870
|
+
const lines = [`const ${valueAlias} = ${valuePath};`, `if (${valueAlias} !== undefined && ${valueAlias} !== null) {`];
|
|
871
|
+
lines.push(`if (typeof ${valueAlias} !== '${pt}') { ${emit!(`reg[${typeMsgIdx}][typeof ${valueAlias}]`)} }`);
|
|
872
|
+
if (pt === 'number') lines.push(`else if (${valueAlias} !== ${valueAlias}) { ${emit!(`"Invalid value NaN, expected a valid ${iname}"`)} }`);
|
|
873
|
+
lines.push(`}`);
|
|
874
|
+
return lines.join('\n');
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== undefined && ${valueAlias} !== null) { ${codeGen(inner, context, valueAlias, mode)} }`;
|
|
656
878
|
} else if (tag === TUPLE) {
|
|
657
879
|
const valueAlias = context.unique('v');
|
|
658
880
|
if (mode.fast) {
|
|
@@ -664,12 +886,12 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
664
886
|
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Array\``)} }`,
|
|
665
887
|
`else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
|
|
666
888
|
`else if (${valueAlias}.length !== ${schema.schemas.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.schemas.length}\``)} }`,
|
|
667
|
-
`else { ${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`,
|
|
889
|
+
`else { ${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx, context))).join('\n')} }`,
|
|
668
890
|
].join('\n');
|
|
669
891
|
}
|
|
670
892
|
} else if (tag === CHECK) {
|
|
671
893
|
const valueAlias = context.unique('v');
|
|
672
|
-
const ref = (v: unknown) => `
|
|
894
|
+
const ref = (v: unknown) => `reg[${context.register(v)}]`;
|
|
673
895
|
const { check: cond, message } = (schema as CheckShape).compile(valueAlias, { ref });
|
|
674
896
|
if (mode.fast) {
|
|
675
897
|
return `const ${valueAlias} = ${valuePath};\nif (${cond}) { ${fail} }`;
|
|
@@ -693,31 +915,39 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
693
915
|
variants.push({ value: discriminantValue, schema: s });
|
|
694
916
|
}
|
|
695
917
|
|
|
918
|
+
const genVariantProps = (s: Schema<T>, variantMode: Mode) => {
|
|
919
|
+
const obj = s as Record<string, unknown>;
|
|
920
|
+
return Object.entries(obj)
|
|
921
|
+
.filter(([k]) => k !== key)
|
|
922
|
+
.map(([k, ps]) =>
|
|
923
|
+
codeGen(ps as Schema<T>, context, `${valueAlias}[${JSON.stringify(k)}]`, variantMode.fast ? variantMode : childMode(variantMode, k, context)),
|
|
924
|
+
)
|
|
925
|
+
.join('\n');
|
|
926
|
+
};
|
|
927
|
+
|
|
696
928
|
if (mode.fast) {
|
|
697
929
|
const branches = variants.map(({ value, schema: s }) => {
|
|
698
|
-
|
|
699
|
-
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
|
|
930
|
+
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${genVariantProps(s, mode)} }`;
|
|
700
931
|
});
|
|
701
932
|
return [
|
|
702
933
|
`const ${valueAlias} = ${valuePath};`,
|
|
703
|
-
`if (${valueAlias} === null || ${valueAlias} === undefined || typeof ${valueAlias} !== 'object'
|
|
934
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined || typeof ${valueAlias} !== 'object'${errChk(valueAlias)}) { ${fail} }`,
|
|
704
935
|
`const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
|
|
705
936
|
branches.join(' else ') + ` else { ${fail} }`,
|
|
706
937
|
].join('\n');
|
|
707
938
|
} else {
|
|
708
939
|
const validValues = variants.map((v) => JSON.stringify(v.value)).join(', ');
|
|
709
940
|
const branches = variants.map(({ value, schema: s }) => {
|
|
710
|
-
|
|
711
|
-
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
|
|
941
|
+
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${genVariantProps(s, mode)} }`;
|
|
712
942
|
});
|
|
713
943
|
return [
|
|
714
944
|
`const ${valueAlias} = ${valuePath};`,
|
|
715
945
|
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
716
946
|
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an object\``)} }`,
|
|
717
|
-
|
|
947
|
+
`${errBranch(valueAlias)}`,
|
|
718
948
|
`else {`,
|
|
719
949
|
` const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
|
|
720
|
-
` ${branches.join(' else ')} else { ${emit!(
|
|
950
|
+
` ${branches.join(' else ')} else { ${emit!(`"Invalid discriminant value " + String(${discriminantAlias}) + ", expected one of: ${validValues.replace(/"/g, "'")}"`)} }`,
|
|
721
951
|
`}`,
|
|
722
952
|
].join('\n');
|
|
723
953
|
}
|
|
@@ -734,7 +964,7 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
734
964
|
if (mode.fast) {
|
|
735
965
|
if (primitiveType) {
|
|
736
966
|
const checks = [`typeof ${valueAlias} !== '${primitiveType}'`];
|
|
737
|
-
if (primitiveType === 'number') checks.push(
|
|
967
|
+
if (primitiveType === 'number') checks.push(`${valueAlias} !== ${valueAlias}`);
|
|
738
968
|
return `const ${valueAlias} = ${valuePath};\nif (${checks.join(' || ')}) { ${fail} }`;
|
|
739
969
|
} else if (name === 'Function') {
|
|
740
970
|
return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== 'function') { ${fail} }`;
|
|
@@ -742,27 +972,32 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
742
972
|
const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
|
|
743
973
|
const index = context.register(schema);
|
|
744
974
|
const registryAlias = context.unique('r');
|
|
745
|
-
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} =
|
|
975
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = reg[${index}];\nif (${valueAlias} === null || ${valueAlias} === undefined${isError || context.pure ? '' : ` || (typeof ${valueAlias} === 'object' && ${valueAlias}[err] === true)`} || (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) || (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) || Number.isNaN(${valueAlias}?.valueOf?.())) { ${fail} }`;
|
|
746
976
|
}
|
|
747
977
|
} else {
|
|
748
978
|
const code: string[] = [`const ${valueAlias} = ${valuePath};`];
|
|
749
979
|
if (primitiveType) {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
980
|
+
const typeMsgs = Object.fromEntries(
|
|
981
|
+
['string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'object', 'function'].map((t) => [t, `Invalid type ${t}, expected type ${name}`]),
|
|
982
|
+
);
|
|
983
|
+
const typeMsgIdx = context.register(typeMsgs);
|
|
984
|
+
code.push(
|
|
985
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`${valueAlias} === null ? "Invalid value null, expected non-nullable" : "Invalid value undefined, expected non-nullable"`)} }`,
|
|
986
|
+
);
|
|
987
|
+
code.push(`${errBranch(valueAlias)}`);
|
|
988
|
+
code.push(`else if (typeof ${valueAlias} !== '${primitiveType}') { ${emit!(`reg[${typeMsgIdx}][typeof ${valueAlias}]`)} }`);
|
|
989
|
+
if (primitiveType === 'number') code.push(`else if (${valueAlias} !== ${valueAlias}) { ${emit!(`"Invalid value NaN, expected a valid ${name}"`)} }`);
|
|
755
990
|
} else if (name === 'Function') {
|
|
756
991
|
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
757
|
-
code.push(
|
|
992
|
+
code.push(`${errBranch(valueAlias)}`);
|
|
758
993
|
code.push(`else if (typeof ${valueAlias} !== 'function') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected type Function\``)} }`);
|
|
759
994
|
} else {
|
|
760
995
|
const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
|
|
761
996
|
const index = context.register(schema);
|
|
762
997
|
const registryAlias = context.unique('r');
|
|
763
|
-
code.push(`const ${registryAlias} =
|
|
998
|
+
code.push(`const ${registryAlias} = reg[${index}];`);
|
|
764
999
|
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
765
|
-
if (!isError) code.push(
|
|
1000
|
+
if (!isError) code.push(`${errBranch(valueAlias)}`);
|
|
766
1001
|
code.push(
|
|
767
1002
|
`else if (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}?.constructor?.name}, expected an instance of ${name}\``)} }`,
|
|
768
1003
|
);
|
|
@@ -789,11 +1024,15 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
789
1024
|
}
|
|
790
1025
|
return code;
|
|
791
1026
|
} else {
|
|
1027
|
+
const arrTypeMsgs = Object.fromEntries(
|
|
1028
|
+
['string', 'number', 'boolean', 'bigint', 'symbol', 'undefined', 'function'].map((t) => [t, `Invalid type ${t}, expected an instance of Array`]),
|
|
1029
|
+
);
|
|
1030
|
+
const arrTypeMsgIdx = context.register(arrTypeMsgs);
|
|
792
1031
|
const code: string[] = [
|
|
793
1032
|
`const ${valueAlias} = ${valuePath};`,
|
|
794
|
-
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(
|
|
795
|
-
|
|
796
|
-
`else if (typeof ${valueAlias} !== 'object') { ${emit!(
|
|
1033
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`${valueAlias} === null ? "Invalid value null, expected non-nullable" : "Invalid value undefined, expected non-nullable"`)} }`,
|
|
1034
|
+
`${errBranch(valueAlias)}`,
|
|
1035
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`reg[${arrTypeMsgIdx}][typeof ${valueAlias}]`)} }`,
|
|
797
1036
|
`else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
|
|
798
1037
|
];
|
|
799
1038
|
if (schema.length > 0) {
|
|
@@ -802,13 +1041,13 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
802
1041
|
if (schema.length === 1) {
|
|
803
1042
|
// Dynamic key - use runtime concat
|
|
804
1043
|
code.push(
|
|
805
|
-
`else { for (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, childMode(mode, { dynamic: key }))} } }`,
|
|
1044
|
+
`else { for (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, childMode(mode, { dynamic: key }, context))} } }`,
|
|
806
1045
|
);
|
|
807
1046
|
} else {
|
|
808
1047
|
code.push(
|
|
809
1048
|
`else if (${valueAlias}.length > ${schema.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.length}\``)} }`,
|
|
810
1049
|
);
|
|
811
|
-
code.push(`else { ${schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`);
|
|
1050
|
+
code.push(`else { ${schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx, context))).join('\n')} }`);
|
|
812
1051
|
}
|
|
813
1052
|
}
|
|
814
1053
|
return code.join('\n');
|
|
@@ -819,73 +1058,122 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
819
1058
|
if (schema instanceof RegExp) {
|
|
820
1059
|
const valueAlias = context.unique('v');
|
|
821
1060
|
if (mode.fast) {
|
|
822
|
-
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined
|
|
1061
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined${errChk(valueAlias)} || !${schema.toString()}.test(String(${valueAlias}))) { ${fail} }`;
|
|
823
1062
|
} else {
|
|
824
|
-
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }\
|
|
1063
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }\n${errBranch(valueAlias)}\n${context.pure ? '' : 'else '}if (!${schema.toString()}.test(String(${valueAlias}))) { ${emit!(`\`Invalid value \${${valueAlias}}, expected to match ${schema.toString()}\``)} }`;
|
|
825
1064
|
}
|
|
826
1065
|
} else {
|
|
827
1066
|
const valueAlias = context.unique('v');
|
|
828
1067
|
if (mode.fast) {
|
|
829
|
-
|
|
1068
|
+
const indexed = mode.indexed && !mode.onFail;
|
|
1069
|
+
const rootFail = indexed ? 'return 0;' : fail;
|
|
1070
|
+
let code = `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || typeof ${valueAlias} !== 'object'${errChk(valueAlias)}) { ${rootFail} }`;
|
|
830
1071
|
if ($keys in schema) {
|
|
831
|
-
const keysAlias = context.unique('k');
|
|
832
1072
|
const kAlias = context.unique('k');
|
|
833
|
-
code += `\
|
|
1073
|
+
code += `\nfor (const ${kAlias} in ${valueAlias}) { ${codeGen(schema[$keys], context, kAlias, indexed ? { fast: true, onFail: 'return 0;' } : mode)} }`;
|
|
834
1074
|
}
|
|
835
1075
|
if ($values in schema) {
|
|
836
|
-
const vAlias = context.unique('val');
|
|
837
1076
|
const kAlias = context.unique('k');
|
|
838
|
-
const
|
|
839
|
-
code += `\nconst ${entriesAlias} = Object.entries(${valueAlias});\nfor (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGen(schema[$values], context, vAlias, mode)} }`;
|
|
1077
|
+
code += `\nfor (const ${kAlias} in ${valueAlias}) { ${codeGen(schema[$values], context, `${valueAlias}[${kAlias}]`, indexed ? { fast: true, onFail: 'return 0;' } : mode)} }`;
|
|
840
1078
|
}
|
|
841
1079
|
if ($strict in schema && schema[$strict]) {
|
|
842
|
-
const
|
|
1080
|
+
const allowedRef = `reg[${context.register(Object.fromEntries(Object.keys(schema).map((k) => [k, 1])))}]`;
|
|
843
1081
|
const kAlias = context.unique('k');
|
|
844
|
-
const
|
|
845
|
-
code += `\nconst ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});\nconst ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));\nif (${extraAlias}.length !== 0) { ${fail} }`;
|
|
1082
|
+
code += `\nfor (const ${kAlias} in ${valueAlias}) { if (!Object.hasOwn(${allowedRef}, ${kAlias})) { ${rootFail} } }`;
|
|
846
1083
|
}
|
|
1084
|
+
const entries = Object.entries(schema);
|
|
847
1085
|
code +=
|
|
848
1086
|
'\n' +
|
|
849
|
-
|
|
850
|
-
.map(([key, s]) =>
|
|
1087
|
+
entries
|
|
1088
|
+
.map(([key, s], idx) => {
|
|
1089
|
+
const propMode: Mode = indexed ? { fast: true, onFail: `return ${idx + 1};` } : mode;
|
|
1090
|
+
return codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, propMode);
|
|
1091
|
+
})
|
|
851
1092
|
.join('\n');
|
|
1093
|
+
if (indexed) code += `\nreturn -1;`;
|
|
852
1094
|
return code;
|
|
853
1095
|
} else {
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1096
|
+
const sv = !mode.fast && mode.startVar;
|
|
1097
|
+
const code: string[] = [`const ${valueAlias} = ${valuePath};`];
|
|
1098
|
+
|
|
1099
|
+
if (sv) {
|
|
1100
|
+
const rootCase: string[] = [];
|
|
1101
|
+
if (mode.firstError) {
|
|
1102
|
+
rootCase.push(
|
|
1103
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
1104
|
+
);
|
|
1105
|
+
rootCase.push(
|
|
1106
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Object\``)} }`,
|
|
1107
|
+
);
|
|
1108
|
+
rootCase.push(`${errBranch(valueAlias)}`);
|
|
1109
|
+
} else {
|
|
1110
|
+
rootCase.push(
|
|
1111
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} break; }`,
|
|
1112
|
+
);
|
|
1113
|
+
rootCase.push(
|
|
1114
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Object\``)} break; }`,
|
|
1115
|
+
);
|
|
1116
|
+
if (!context.pure)
|
|
1117
|
+
rootCase.push(`else if (typeof ${valueAlias} === 'object' && ${valueAlias}[err] === true) { ${emit!(`\`\${${valueAlias}.message}\``)} break; }`);
|
|
1118
|
+
}
|
|
1119
|
+
if ($keys in schema) {
|
|
1120
|
+
const kAlias = context.unique('k');
|
|
1121
|
+
rootCase.push(
|
|
1122
|
+
`for (const ${kAlias} in ${valueAlias}) { ${codeGen(schema[$keys], context, kAlias, childMode(mode, { dynamic: kAlias }, context))} }`,
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
if ($values in schema) {
|
|
1126
|
+
const kAlias = context.unique('k');
|
|
1127
|
+
rootCase.push(
|
|
1128
|
+
`for (const ${kAlias} in ${valueAlias}) { ${codeGen(schema[$values], context, `${valueAlias}[${kAlias}]`, childMode(mode, { dynamic: kAlias }, context))} }`,
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
if ($strict in schema && schema[$strict]) {
|
|
1132
|
+
const allowedRef = `reg[${context.register(Object.fromEntries(Object.keys(schema).map((k) => [k, 1])))}]`;
|
|
1133
|
+
const kAlias = context.unique('k');
|
|
1134
|
+
const extraAlias = context.unique('ex');
|
|
1135
|
+
rootCase.push(`const ${extraAlias} = [];`);
|
|
1136
|
+
rootCase.push(`for (const ${kAlias} in ${valueAlias}) { if (!Object.hasOwn(${allowedRef}, ${kAlias})) ${extraAlias}.push(${kAlias}); }`);
|
|
1137
|
+
rootCase.push(`if (${extraAlias}.length !== 0) { ${emit!(`\`Extra properties: \${${extraAlias}}, are not allowed\``)} }`);
|
|
1138
|
+
}
|
|
1139
|
+
code.push(`switch (${sv}) {`);
|
|
1140
|
+
code.push(`case 0: { ${rootCase.join('\n')} }`);
|
|
1141
|
+
const entries = Object.entries(schema);
|
|
1142
|
+
entries.forEach(([key, s], idx) => {
|
|
1143
|
+
const propCode = codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, childMode(mode, key, context));
|
|
1144
|
+
code.push(`case ${idx + 1}: { ${propCode} }`);
|
|
1145
|
+
});
|
|
1146
|
+
code.push(`}`);
|
|
1147
|
+
return code.join('\n');
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
1151
|
+
code.push(`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Object\``)} }`);
|
|
1152
|
+
code.push(`${errBranch(valueAlias)}`);
|
|
1153
|
+
code.push('else {');
|
|
861
1154
|
const innerCode: string[] = [];
|
|
862
1155
|
if ($keys in schema) {
|
|
863
|
-
const keysAlias = context.unique('k');
|
|
864
1156
|
const kAlias = context.unique('k');
|
|
865
|
-
innerCode.push(
|
|
866
|
-
|
|
867
|
-
|
|
1157
|
+
innerCode.push(
|
|
1158
|
+
`for (const ${kAlias} in ${valueAlias}) { ${codeGen(schema[$keys], context, kAlias, childMode(mode, { dynamic: kAlias }, context))} }`,
|
|
1159
|
+
);
|
|
868
1160
|
}
|
|
869
1161
|
if ($values in schema) {
|
|
870
|
-
const vAlias = context.unique('val');
|
|
871
1162
|
const kAlias = context.unique('k');
|
|
872
|
-
const entriesAlias = context.unique('en');
|
|
873
|
-
innerCode.push(`const ${entriesAlias} = Object.entries(${valueAlias});`);
|
|
874
|
-
// Dynamic key - use runtime concat
|
|
875
1163
|
innerCode.push(
|
|
876
|
-
`for (const
|
|
1164
|
+
`for (const ${kAlias} in ${valueAlias}) { ${codeGen(schema[$values], context, `${valueAlias}[${kAlias}]`, childMode(mode, { dynamic: kAlias }, context))} }`,
|
|
877
1165
|
);
|
|
878
1166
|
}
|
|
879
1167
|
if ($strict in schema && schema[$strict]) {
|
|
880
|
-
const
|
|
1168
|
+
const allowedRef = `reg[${context.register(Object.fromEntries(Object.keys(schema).map((k) => [k, 1])))}]`;
|
|
881
1169
|
const kAlias = context.unique('k');
|
|
882
1170
|
const extraAlias = context.unique('ex');
|
|
883
|
-
innerCode.push(`const ${
|
|
884
|
-
innerCode.push(`const ${
|
|
1171
|
+
innerCode.push(`const ${extraAlias} = [];`);
|
|
1172
|
+
innerCode.push(`for (const ${kAlias} in ${valueAlias}) { if (!Object.hasOwn(${allowedRef}, ${kAlias})) ${extraAlias}.push(${kAlias}); }`);
|
|
885
1173
|
innerCode.push(`if (${extraAlias}.length !== 0) { ${emit!(`\`Extra properties: \${${extraAlias}}, are not allowed\``)} }`);
|
|
886
1174
|
}
|
|
887
|
-
|
|
888
|
-
innerCode.push(...
|
|
1175
|
+
const entries = Object.entries(schema);
|
|
1176
|
+
innerCode.push(...entries.map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, childMode(mode, key, context))));
|
|
889
1177
|
code.push(innerCode.join('\n'), '}');
|
|
890
1178
|
return code.join('\n');
|
|
891
1179
|
}
|
|
@@ -897,9 +1185,9 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
897
1185
|
const valueAlias = context.unique('v');
|
|
898
1186
|
const registryAlias = context.unique('r');
|
|
899
1187
|
if (mode.fast) {
|
|
900
|
-
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} =
|
|
1188
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = reg[${index}];\nif (typeof ${valueAlias} !== 'symbol' || ${valueAlias} !== ${registryAlias}) { ${fail} }`;
|
|
901
1189
|
} else {
|
|
902
|
-
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} =
|
|
1190
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = reg[${index}];\nif (typeof ${valueAlias} !== 'symbol') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected symbol\``)} }\nelse if (${valueAlias} !== ${registryAlias}) { ${emit!(`\`Invalid value \${${valueAlias}.toString()}, expected ${schema.toString()}\``)} }`;
|
|
903
1191
|
}
|
|
904
1192
|
}
|
|
905
1193
|
|
|
@@ -917,7 +1205,9 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
917
1205
|
return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== '${typeof schema}' || ${valueAlias} !== ${toLiteral(schema)}) { ${fail} }`;
|
|
918
1206
|
} else {
|
|
919
1207
|
const value = context.unique('val');
|
|
920
|
-
return
|
|
1208
|
+
return context.pure
|
|
1209
|
+
? `const ${valueAlias} = ${valuePath};\nconst ${value} = ${toLiteral(schema)};\nif (typeof ${valueAlias} !== '${typeof schema}') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected ${typeof schema}\``)} }\nelse if (${valueAlias} !== ${value}) { ${emit!(`\`Invalid value \${String(${valueAlias})}, expected ${toLiteral(schema)}\``)} }`
|
|
1210
|
+
: `const ${valueAlias} = ${valuePath};\nconst ${value} = ${toLiteral(schema)};\nif (typeof ${valueAlias} === 'object' && ${valueAlias} !== null && ${valueAlias}[err] === true) { ${emit!(`\`\${${valueAlias}.message}\``)} }\nelse if (typeof ${valueAlias} !== '${typeof schema}') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected ${typeof schema}\``)} }\nelse if (${valueAlias} !== ${value}) { ${emit!(`\`Invalid value \${String(${valueAlias})}, expected ${toLiteral(schema)}\``)} }`;
|
|
921
1211
|
}
|
|
922
1212
|
};
|
|
923
1213
|
|
|
@@ -942,6 +1232,12 @@ export interface CompileOptions {
|
|
|
942
1232
|
* Default is false (first-error mode) for optimal performance.
|
|
943
1233
|
*/
|
|
944
1234
|
allErrors?: boolean;
|
|
1235
|
+
/**
|
|
1236
|
+
* When true, skips `instanceof Error` checks in the fast validator.
|
|
1237
|
+
* Enable this when the data does not come from `as.*` converters for maximum performance.
|
|
1238
|
+
* Error messages in the issue validator are unaffected.
|
|
1239
|
+
*/
|
|
1240
|
+
pure?: boolean;
|
|
945
1241
|
}
|
|
946
1242
|
|
|
947
1243
|
/**
|
|
@@ -987,15 +1283,47 @@ export interface CompileOptions {
|
|
|
987
1283
|
*/
|
|
988
1284
|
export const compile = <T>(schema: Schema<T>, options?: CompileOptions): Validator<T> => {
|
|
989
1285
|
const allErrors = options?.allErrors ?? false;
|
|
1286
|
+
const pure = options?.pure ?? false;
|
|
1287
|
+
const canIndex = typeof schema === 'object' && schema !== null && !isTagged(schema) && !Array.isArray(schema) && !(schema instanceof RegExp);
|
|
990
1288
|
|
|
991
1289
|
if (allErrors) {
|
|
1290
|
+
if (canIndex) {
|
|
1291
|
+
const fastContext = new Context();
|
|
1292
|
+
fastContext.pure = pure;
|
|
1293
|
+
const fastCode = codeGen(schema, fastContext, 'data', { fast: true, indexed: true });
|
|
1294
|
+
const indexedFast = new Function('reg', 'err', `return (data) => {\n${fastCode}\n};`)(fastContext.registry, ERR) as (data: T) => number;
|
|
1295
|
+
|
|
1296
|
+
const issueContext = new Context();
|
|
1297
|
+
issueContext.pure = pure;
|
|
1298
|
+
const rootPath = `reg[${issueContext.register(Object.freeze([]))}]`;
|
|
1299
|
+
const issueCode = `let issues = [];\n${codeGen(schema, issueContext, 'data', { fast: false, firstError: false, issues: 'issues', path: [], pathExpr: rootPath, startVar: 'start', issuesReady: true })}\nreturn issues;`;
|
|
1300
|
+
const issueValidator = new Function('reg', 'err', `return (data, start) => {\n${issueCode}\n};`)(issueContext.registry, ERR) as (
|
|
1301
|
+
data: T,
|
|
1302
|
+
start: number,
|
|
1303
|
+
) => StandardSchemaV1.Issue[];
|
|
1304
|
+
|
|
1305
|
+
const validator = ((data: T): boolean => {
|
|
1306
|
+
const idx = indexedFast(data);
|
|
1307
|
+
if (idx === -1) return true;
|
|
1308
|
+
validator.issues = issueValidator(data, idx);
|
|
1309
|
+
return false;
|
|
1310
|
+
}) as Validator<T>;
|
|
1311
|
+
validator.issues = emptyIssues;
|
|
1312
|
+
return validator;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
992
1315
|
const fastContext = new Context();
|
|
1316
|
+
fastContext.pure = pure;
|
|
993
1317
|
const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
|
|
994
|
-
const fastValidator = new Function('
|
|
1318
|
+
const fastValidator = new Function('reg', 'err', `return (data) => {\n${fastCode}\n};`)(fastContext.registry, ERR) as (data: T) => boolean;
|
|
995
1319
|
|
|
996
1320
|
const issueContext = new Context();
|
|
997
|
-
|
|
998
|
-
const
|
|
1321
|
+
issueContext.pure = pure;
|
|
1322
|
+
const rootPath2 = `reg[${issueContext.register(Object.freeze([]))}]`;
|
|
1323
|
+
const issueCode = `let issues = [];\n${codeGen(schema, issueContext, 'data', { fast: false, firstError: false, issues: 'issues', path: [], pathExpr: rootPath2, issuesReady: true })}\nreturn issues;`;
|
|
1324
|
+
const issueValidator = new Function('reg', 'err', `return (data) => {\n${issueCode}\n};`)(issueContext.registry, ERR) as (
|
|
1325
|
+
data: T,
|
|
1326
|
+
) => StandardSchemaV1.Issue[];
|
|
999
1327
|
|
|
1000
1328
|
const validator = ((data: T): boolean => {
|
|
1001
1329
|
if (fastValidator(data)) {
|
|
@@ -1008,13 +1336,43 @@ export const compile = <T>(schema: Schema<T>, options?: CompileOptions): Validat
|
|
|
1008
1336
|
return validator;
|
|
1009
1337
|
}
|
|
1010
1338
|
|
|
1339
|
+
if (canIndex) {
|
|
1340
|
+
const fastContext = new Context();
|
|
1341
|
+
fastContext.pure = pure;
|
|
1342
|
+
const fastCode = codeGen(schema, fastContext, 'data', { fast: true, indexed: true });
|
|
1343
|
+
const indexedFast = new Function('reg', 'err', `return (data) => {\n${fastCode}\n};`)(fastContext.registry, ERR) as (data: T) => number;
|
|
1344
|
+
|
|
1345
|
+
const context = new Context();
|
|
1346
|
+
context.pure = pure;
|
|
1347
|
+
const rootPath = `reg[${context.register(Object.freeze([]))}]`;
|
|
1348
|
+
const code = codeGen(schema, context, 'data', { fast: false, firstError: true, issues: 'issues', path: [], pathExpr: rootPath, startVar: 'start' });
|
|
1349
|
+
const firstErrorValidator = new Function('reg', 'err', `return (data, start) => {\nlet issues;\n${code}\nreturn issues;\n};`)(context.registry, ERR) as (
|
|
1350
|
+
data: T,
|
|
1351
|
+
start: number,
|
|
1352
|
+
) => StandardSchemaV1.Issue[] | undefined;
|
|
1353
|
+
|
|
1354
|
+
const validator = ((data: T): boolean => {
|
|
1355
|
+
const idx = indexedFast(data);
|
|
1356
|
+
if (idx === -1) {
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
validator.issues = firstErrorValidator(data, idx)!;
|
|
1360
|
+
return false;
|
|
1361
|
+
}) as Validator<T>;
|
|
1362
|
+
validator.issues = emptyIssues;
|
|
1363
|
+
return validator;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1011
1366
|
const fastContext = new Context();
|
|
1367
|
+
fastContext.pure = pure;
|
|
1012
1368
|
const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
|
|
1013
|
-
const fastValidator = new Function('
|
|
1369
|
+
const fastValidator = new Function('reg', 'err', `return (data) => {\n${fastCode}\n};`)(fastContext.registry, ERR) as (data: T) => boolean;
|
|
1014
1370
|
|
|
1015
1371
|
const context = new Context();
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1372
|
+
context.pure = pure;
|
|
1373
|
+
const rootPath3 = `reg[${context.register(Object.freeze([]))}]`;
|
|
1374
|
+
const code = codeGen(schema, context, 'data', { fast: false, firstError: true, issues: 'issues', path: [], pathExpr: rootPath3 });
|
|
1375
|
+
const firstErrorValidator = new Function('reg', 'err', `return (data) => {\nlet issues;\n${code}\nreturn issues;\n};`)(context.registry, ERR) as (
|
|
1018
1376
|
data: T,
|
|
1019
1377
|
) => StandardSchemaV1.Issue[] | undefined;
|
|
1020
1378
|
|