@amritk/generate-validators 0.10.0 → 0.10.1
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.
|
@@ -580,7 +580,12 @@ const generateObjectValidator = (schema, typeName, suffix) => {
|
|
|
580
580
|
if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames)) {
|
|
581
581
|
propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix));
|
|
582
582
|
}
|
|
583
|
-
|
|
583
|
+
// Lazily allocate the errors array so a valid input never builds one — the same
|
|
584
|
+
// allocation-free happy path the runtime interpreter uses. Each emitted
|
|
585
|
+
// `errors.push(...)` becomes a create-on-first-use push; nothing is allocated
|
|
586
|
+
// until the first actual error, so the common valid case stays alloc-free even
|
|
587
|
+
// when the schema is too rich for the boolean guard.
|
|
588
|
+
const body = (propertyLines.length > 0 ? '\n' + propertyLines.join('\n') + '\n' : '').replaceAll('errors.push(', '(errors ??= []).push(');
|
|
584
589
|
// Hoisted statements (e.g. known-keys Sets) come first so every call of the
|
|
585
590
|
// validator reuses them instead of rebuilding them.
|
|
586
591
|
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join('\n')}\n\n` : '';
|
|
@@ -602,9 +607,9 @@ const generateObjectValidator = (schema, typeName, suffix) => {
|
|
|
602
607
|
` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
|
|
603
608
|
` }`,
|
|
604
609
|
``,
|
|
605
|
-
`
|
|
610
|
+
` let errors: ValidationError[] | undefined`,
|
|
606
611
|
body,
|
|
607
|
-
` return errors
|
|
612
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
608
613
|
`}`,
|
|
609
614
|
].join('\n');
|
|
610
615
|
// No guard: the exported validator is the error-collecting function itself.
|
|
@@ -661,6 +666,23 @@ const rightTypeCondition = (accessor, type) => {
|
|
|
661
666
|
return null;
|
|
662
667
|
}
|
|
663
668
|
};
|
|
669
|
+
/**
|
|
670
|
+
* Builds the membership test for an `enum`, matching the slow path's
|
|
671
|
+
* `[...].includes(value)` verdict exactly. For the common all-primitive case it
|
|
672
|
+
* emits a parenthesized `a === x || a === y` chain — no per-call array
|
|
673
|
+
* allocation and no linear scan, so it stays on the allocation-free hot path —
|
|
674
|
+
* and falls back to `.includes` when a member is an object/array (reference
|
|
675
|
+
* equality) or `NaN` (where `includes`'s SameValueZero differs from `===`).
|
|
676
|
+
*/
|
|
677
|
+
const enumMembershipExpr = (values, acc) => {
|
|
678
|
+
const allPrimitive = values.length > 0 &&
|
|
679
|
+
values.every((v) => (v === null || typeof v !== 'object') && typeof v !== 'function') &&
|
|
680
|
+
!values.some((v) => typeof v === 'number' && Number.isNaN(v));
|
|
681
|
+
if (allPrimitive) {
|
|
682
|
+
return `(${values.map((v) => `${acc} === ${JSON.stringify(v)}`).join(' || ')})`;
|
|
683
|
+
}
|
|
684
|
+
return `(${JSON.stringify(values)} as unknown[]).includes(${acc})`;
|
|
685
|
+
};
|
|
664
686
|
/**
|
|
665
687
|
* Builds a boolean expression that is TRUE iff `acc` satisfies `schema`, with the
|
|
666
688
|
* *exact same verdict* as the error-collecting validator — or `null` when the
|
|
@@ -686,7 +708,7 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
686
708
|
}
|
|
687
709
|
// enum — same membership test the validator uses.
|
|
688
710
|
if (hasEnum(schema)) {
|
|
689
|
-
return
|
|
711
|
+
return enumMembershipExpr(schema.enum, acc);
|
|
690
712
|
}
|
|
691
713
|
if (!hasType(schema))
|
|
692
714
|
return null;
|
|
@@ -738,6 +760,12 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
738
760
|
* array shape and — for typed items — only each item's *type* (objects are shape-
|
|
739
761
|
* checked, not recursed into); it never enforces `minItems`/`maxItems` or item
|
|
740
762
|
* constraints. Returns `null` for `$ref` items (those defer to the validator).
|
|
763
|
+
*
|
|
764
|
+
* Item iteration goes through `Array.from` rather than `Array.prototype.every`
|
|
765
|
+
* because `every` *skips holes* in a sparse array (`[, 'x']`), whereas the
|
|
766
|
+
* validator's index-based `for` loop reads a hole as `undefined` and rejects it.
|
|
767
|
+
* Materialising the array first makes the guard's verdict match the slow path's
|
|
768
|
+
* on sparse input — the guard must never accept what the slow path would reject.
|
|
741
769
|
*/
|
|
742
770
|
const booleanArrayExpr = (schema, acc) => {
|
|
743
771
|
const base = `Array.isArray(${acc})`;
|
|
@@ -753,7 +781,7 @@ const booleanArrayExpr = (schema, acc) => {
|
|
|
753
781
|
const itemCheck = rightTypeCondition('_it', items.type);
|
|
754
782
|
if (itemCheck === null)
|
|
755
783
|
return base;
|
|
756
|
-
return `${base} && (${acc} as unknown[]).every((_it) => ${itemCheck})`;
|
|
784
|
+
return `${base} && Array.from(${acc} as unknown[]).every((_it) => ${itemCheck})`;
|
|
757
785
|
};
|
|
758
786
|
/**
|
|
759
787
|
* Builds the `&&` conditions proving an object value is valid (same verdict as
|
|
@@ -978,9 +1006,9 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
978
1006
|
` if (${wrongType}) {`,
|
|
979
1007
|
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
980
1008
|
` }`,
|
|
981
|
-
`
|
|
982
|
-
constraintLines.join('\n'),
|
|
983
|
-
` return errors
|
|
1009
|
+
` let errors: ValidationError[] | undefined`,
|
|
1010
|
+
constraintLines.join('\n').replaceAll('errors.push(', '(errors ??= []).push('),
|
|
1011
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
984
1012
|
`}`,
|
|
985
1013
|
].join('\n');
|
|
986
1014
|
}
|