@amritk/generate-parsers 0.16.0 → 0.16.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.
- package/README.md +45 -0
- package/dist/generators/assert-supported-keywords.d.ts +26 -0
- package/dist/generators/assert-supported-keywords.js +74 -0
- package/dist/generators/generate-parser-function.js +136 -52
- package/dist/generators/generate-schema-checks.js +161 -1
- package/dist/generators/generate-strict-assertion.d.ts +22 -1
- package/dist/generators/generate-strict-assertion.js +213 -12
- package/dist/generators/generate-type-checks.d.ts +9 -0
- package/dist/generators/generate-type-checks.js +49 -1
- package/dist/generators/generate-validation-expression.d.ts +23 -0
- package/dist/generators/generate-validation-expression.js +72 -3
- package/dist/generators/subschema-match.d.ts +29 -0
- package/dist/generators/subschema-match.js +286 -0
- package/package.json +2 -2
|
@@ -8,10 +8,11 @@ import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
|
|
|
8
8
|
import { findDiscriminator } from '../helpers/find-discriminator.js';
|
|
9
9
|
import { getDefaultValue } from '../helpers/get-default-value.js';
|
|
10
10
|
import { getDiscriminatorValue } from '../helpers/get-discriminator-value.js';
|
|
11
|
+
import { assertNoUnsupportedKeywords } from './assert-supported-keywords.js';
|
|
11
12
|
import { generateEnumCaseInsensitiveCoercion } from './generate-enum-check.js';
|
|
12
|
-
import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
|
|
13
|
-
import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
|
|
14
|
-
import { generateValidationExpression, isCoercibleItemSchema } from './generate-validation-expression.js';
|
|
13
|
+
import { generateContainsCheck, generateObjectKeywordChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, } from './generate-strict-assertion.js';
|
|
14
|
+
import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
|
|
15
|
+
import { generatePrefixItemsMap, generateValidationExpression, getPrefixItems, isCoercibleItemSchema, prefixItemsCapsLength, } from './generate-validation-expression.js';
|
|
15
16
|
/**
|
|
16
17
|
* Generates the parser function name from a type name.
|
|
17
18
|
* Converts "UserObject" to "parseUserObject".
|
|
@@ -19,6 +20,16 @@ import { generateValidationExpression, isCoercibleItemSchema } from './generate-
|
|
|
19
20
|
const generateParserName = (typeName) => {
|
|
20
21
|
return `parse${typeName}`;
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Object-literal key form that is safe for the runtime-dangerous name
|
|
25
|
+
* `__proto__`. In an object literal `{ __proto__: v }` / `{ "__proto__": v }` is
|
|
26
|
+
* the special prototype-setter syntax — it never creates an own property — so a
|
|
27
|
+
* schema property literally named `__proto__` would silently vanish (and, for an
|
|
28
|
+
* object value, reassign the result's prototype). The computed form
|
|
29
|
+
* `{ ["__proto__"]: v }` creates a normal own property. Every other key keeps
|
|
30
|
+
* the plain `safeKey` output, so the common path is unchanged.
|
|
31
|
+
*/
|
|
32
|
+
const safeLiteralKey = (key) => (key === '__proto__' ? '["__proto__"]' : safeKey(key));
|
|
22
33
|
/**
|
|
23
34
|
* Checks if a property is required based on the schema's required array.
|
|
24
35
|
*/
|
|
@@ -50,7 +61,7 @@ const generateOptionalRefCall = (key, ref, suffix) => {
|
|
|
50
61
|
// Gate on presence, not truthiness: a `&&` guard skips parsing when the value
|
|
51
62
|
// is `false`/`0`/`""`/`null`, spreading it raw. `!== undefined` still omits an
|
|
52
63
|
// absent optional property but coerces every present one, matching the main path.
|
|
53
|
-
return `...(${acc} !== undefined && { ${
|
|
64
|
+
return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${acc}) })`;
|
|
54
65
|
};
|
|
55
66
|
/**
|
|
56
67
|
* Generates a validateArray call for required array properties with $ref items.
|
|
@@ -68,7 +79,7 @@ const generateOptionalArrayRefCall = (key, ref, suffix) => {
|
|
|
68
79
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
69
80
|
const acc = safeAccessor('input', key);
|
|
70
81
|
// Presence gating (not truthiness) so a falsy-but-present value is still parsed.
|
|
71
|
-
return `...(${acc} !== undefined && { ${
|
|
82
|
+
return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateArray(${acc}, ${parserName}) })`;
|
|
72
83
|
};
|
|
73
84
|
/**
|
|
74
85
|
* Generates a validateRecord call for required object properties with additionalProperties $ref.
|
|
@@ -87,14 +98,14 @@ const generateOptionalRecordRefCall = (key, ref, suffix) => {
|
|
|
87
98
|
const acc = safeAccessor('input', key);
|
|
88
99
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
89
100
|
// Presence gating (not truthiness) so a falsy-but-present value is still parsed.
|
|
90
|
-
return `...(${acc} !== undefined && { ${
|
|
101
|
+
return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${acc}, ${parserName}) })`;
|
|
91
102
|
};
|
|
92
103
|
/**
|
|
93
104
|
* Generates a spread entry for optional inline properties.
|
|
94
105
|
* The value expression is evaluated once and omitted when undefined.
|
|
95
106
|
*/
|
|
96
107
|
const generateOptionalInlineProperty = (key, valueExpression) => {
|
|
97
|
-
return `...((value => value === undefined ? {} : { ${
|
|
108
|
+
return `...((value => value === undefined ? {} : { ${safeLiteralKey(key)}: value })(${valueExpression}))`;
|
|
98
109
|
};
|
|
99
110
|
/**
|
|
100
111
|
* Determines if a property schema should use ref imports for validation.
|
|
@@ -256,7 +267,7 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
|
|
|
256
267
|
if (fallbackValue === 'undefined') {
|
|
257
268
|
return `{} as ${typeName}`;
|
|
258
269
|
}
|
|
259
|
-
requiredProps.push(` ${
|
|
270
|
+
requiredProps.push(` ${safeLiteralKey(key)}: ${fallbackValue},`);
|
|
260
271
|
}
|
|
261
272
|
}
|
|
262
273
|
if (requiredProps.length === 0) {
|
|
@@ -456,6 +467,10 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
456
467
|
!Array.isArray(schema.items)) {
|
|
457
468
|
const items = schema.items;
|
|
458
469
|
const notArrayThrow = `if (!Array.isArray(input)) throw new Error(\`[${typeName}] expected array, got \${input === null ? "null" : typeof input}\`);`;
|
|
470
|
+
// This delegating path bypasses generateScalarStrictAssertion, so enforce the
|
|
471
|
+
// array-level `contains` constraint here too — otherwise a root array of
|
|
472
|
+
// objects/$refs would silently ignore it in strict mode.
|
|
473
|
+
const strictArrayChecks = strict ? generateContainsCheck('input', schema, `[${typeName}]`).join('\n') : '';
|
|
459
474
|
// validateArray identity-returns the input array when every element parses
|
|
460
475
|
// to itself; this parser is EXPORTED, and exported parsers never alias the
|
|
461
476
|
// value the caller passed in (matching the scalar root-array path's
|
|
@@ -463,7 +478,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
463
478
|
// element references are still shared, like every other fast path.
|
|
464
479
|
const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});\n return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
|
|
465
480
|
const delegated = (itemParserName) => strict
|
|
466
|
-
? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}\n${delegatedBody(itemParserName)}\n};`
|
|
481
|
+
? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}${strictArrayChecks ? `\n${strictArrayChecks}` : ''}\n${delegatedBody(itemParserName)}\n};`
|
|
467
482
|
: `export const ${functionName} = (input: unknown): ${typeName} => {\n${delegatedBody(itemParserName)}\n};`;
|
|
468
483
|
if (unionCtx?.useRefImports && isSchemaObject(items) && hasRef(items)) {
|
|
469
484
|
return delegated(generateParserName(refToName(items.$ref, unionCtx.suffix)));
|
|
@@ -495,7 +510,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
495
510
|
return `export const ${functionName} = (input: unknown): ${typeName} => null as ${typeName};`;
|
|
496
511
|
}
|
|
497
512
|
if (strict) {
|
|
498
|
-
const assertion = generateScalarStrictAssertion(schema, typeName);
|
|
513
|
+
const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
|
|
499
514
|
if (assertion === null) {
|
|
500
515
|
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
|
|
501
516
|
}
|
|
@@ -513,6 +528,13 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
513
528
|
case 'boolean':
|
|
514
529
|
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
|
|
515
530
|
case 'array': {
|
|
531
|
+
// Tuple `prefixItems`: coerce each declared position and, under items:false,
|
|
532
|
+
// drop elements past the tuple length. A non-array coerces to an empty array.
|
|
533
|
+
const prefixItems = getPrefixItems(schema);
|
|
534
|
+
if (prefixItems) {
|
|
535
|
+
const mapped = generatePrefixItemsMap('input', prefixItems, prefixItemsCapsLength(schema), unionCtx?.rootSchema, undefined, unionCtx?.caseInsensitive);
|
|
536
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? ${mapped} as ${typeName} : [] as ${typeName};`;
|
|
537
|
+
}
|
|
516
538
|
// Coerce each element when the item schema is a single scalar type or an enum.
|
|
517
539
|
if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
|
|
518
540
|
const item = schema.items;
|
|
@@ -525,15 +547,33 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
525
547
|
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
|
|
526
548
|
}
|
|
527
549
|
};
|
|
550
|
+
/**
|
|
551
|
+
* Object-level keyword checks (`propertyNames` / `dependentSchemas` /
|
|
552
|
+
* `dependentRequired`) rendered as a newline-prefixed prelude for the
|
|
553
|
+
* specialized strict object parsers (empty-object, pattern-properties,
|
|
554
|
+
* additionalProperties, combined) that build their result outside
|
|
555
|
+
* generateObjectStrictAssertion. Each of those emits its own
|
|
556
|
+
* `if (!isObject(input)) throw` first, so the checks run against a proven object
|
|
557
|
+
* (`guard = false`). Returns `''` when the schema carries none of the keywords,
|
|
558
|
+
* so existing output for every other schema is byte-for-byte unchanged.
|
|
559
|
+
*/
|
|
560
|
+
const strictObjectKeywordPrelude = (schema, typeName) => {
|
|
561
|
+
if (!isSchemaObject(schema))
|
|
562
|
+
return '';
|
|
563
|
+
const lines = generateObjectKeywordChecks('input', schema, `[${typeName}]`, false);
|
|
564
|
+
return lines.length > 0 ? `\n${lines.join('\n')}` : '';
|
|
565
|
+
};
|
|
528
566
|
/**
|
|
529
567
|
* Generates a parser for empty object schemas or schemas with only additionalProperties.
|
|
530
568
|
* Validates the input is an object before casting, falling back to an empty object.
|
|
531
|
-
* Returns a shallow copy to avoid mutating the original input.
|
|
569
|
+
* Returns a shallow copy to avoid mutating the original input. `schema`, when
|
|
570
|
+
* given, supplies any object-level keyword checks the strict parser must enforce.
|
|
532
571
|
*/
|
|
533
|
-
const generateEmptyObjectParser = (typeName, strict) => {
|
|
572
|
+
const generateEmptyObjectParser = (typeName, strict, schema) => {
|
|
534
573
|
const functionName = generateParserName(typeName);
|
|
535
574
|
if (strict) {
|
|
536
|
-
|
|
575
|
+
const prelude = schema !== undefined ? strictObjectKeywordPrelude(schema, typeName) : '';
|
|
576
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${prelude}\n return { ...input } as ${typeName};\n};`;
|
|
537
577
|
}
|
|
538
578
|
return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`;
|
|
539
579
|
};
|
|
@@ -674,7 +714,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
674
714
|
const exportPrefix = exported ? 'export ' : '';
|
|
675
715
|
if (!hasProperties(schema)) {
|
|
676
716
|
if (strict) {
|
|
677
|
-
return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`)
|
|
717
|
+
return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${strictObjectKeywordPrelude(schema, typeName)}\n return input as ${typeName};\n};`;
|
|
678
718
|
}
|
|
679
719
|
return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
|
|
680
720
|
}
|
|
@@ -750,7 +790,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
750
790
|
isSchemaObject(schema) &&
|
|
751
791
|
hasAllOf(schema) &&
|
|
752
792
|
schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
|
|
753
|
-
|
|
793
|
+
// Object-level keywords the fast path cannot mirror (dependentRequired,
|
|
794
|
+
// dependentSchemas, propertyNames) force the slow path so the strict assertion
|
|
795
|
+
// actually runs them; returning `{ ...input }` on a bare shape match would skip
|
|
796
|
+
// the enforcement. (A property-level blocker like `contains` is already caught
|
|
797
|
+
// per-property by generatePropertyTypeCheck returning null.)
|
|
798
|
+
let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema);
|
|
754
799
|
const fastPathChecks = [];
|
|
755
800
|
// `toVarName` collapses distinct keys that differ only in non-identifier chars
|
|
756
801
|
// (e.g. `a-b` and `a.b` both → `_a_b`), which would emit two `const _a_b`
|
|
@@ -819,7 +864,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
819
864
|
// stripUnknown input — which carries extras the build removes, and nested
|
|
820
865
|
// extras each sub-parser removes — while the cold path's per-property
|
|
821
866
|
// assertions still produce the precise error on a genuine mismatch.
|
|
822
|
-
let canShallowGuard = strict && stripUnknown && !strictKeys;
|
|
867
|
+
let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
|
|
823
868
|
const shallowChecks = [];
|
|
824
869
|
if (canShallowGuard) {
|
|
825
870
|
for (const { key, varName, isRequired, propSchema } of propInfo) {
|
|
@@ -906,10 +951,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
906
951
|
if (subName) {
|
|
907
952
|
const subParserName = generateParserName(subName);
|
|
908
953
|
if (isRequired) {
|
|
909
|
-
objectLines.push(` ${
|
|
954
|
+
objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
|
|
910
955
|
}
|
|
911
956
|
else {
|
|
912
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
957
|
+
objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
|
|
913
958
|
}
|
|
914
959
|
continue;
|
|
915
960
|
}
|
|
@@ -921,10 +966,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
921
966
|
if (itemSubName) {
|
|
922
967
|
const itemParserName = generateParserName(itemSubName);
|
|
923
968
|
if (isRequired) {
|
|
924
|
-
objectLines.push(` ${
|
|
969
|
+
objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
|
|
925
970
|
}
|
|
926
971
|
else {
|
|
927
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
972
|
+
objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
|
|
928
973
|
}
|
|
929
974
|
continue;
|
|
930
975
|
}
|
|
@@ -933,10 +978,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
933
978
|
const ref = propSchema.$ref;
|
|
934
979
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
935
980
|
if (isRequired) {
|
|
936
|
-
objectLines.push(` ${
|
|
981
|
+
objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
|
|
937
982
|
}
|
|
938
983
|
else {
|
|
939
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
984
|
+
objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${accessor}) }),`);
|
|
940
985
|
}
|
|
941
986
|
continue;
|
|
942
987
|
}
|
|
@@ -946,10 +991,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
946
991
|
const ref = items.$ref;
|
|
947
992
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
948
993
|
if (isRequired) {
|
|
949
|
-
objectLines.push(` ${
|
|
994
|
+
objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
|
|
950
995
|
}
|
|
951
996
|
else {
|
|
952
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
997
|
+
objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
|
|
953
998
|
}
|
|
954
999
|
continue;
|
|
955
1000
|
}
|
|
@@ -959,17 +1004,17 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
959
1004
|
const ref = additionalProps.$ref;
|
|
960
1005
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
961
1006
|
if (isRequired) {
|
|
962
|
-
objectLines.push(` ${
|
|
1007
|
+
objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
|
|
963
1008
|
}
|
|
964
1009
|
else {
|
|
965
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
1010
|
+
objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
|
|
966
1011
|
}
|
|
967
1012
|
continue;
|
|
968
1013
|
}
|
|
969
1014
|
// Handle non-schema-object properties
|
|
970
1015
|
if (!isSchemaObject(propSchema)) {
|
|
971
1016
|
if (isRequired) {
|
|
972
|
-
objectLines.push(` ${
|
|
1017
|
+
objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
|
|
973
1018
|
}
|
|
974
1019
|
continue;
|
|
975
1020
|
}
|
|
@@ -987,10 +1032,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
987
1032
|
valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined, caseInsensitive);
|
|
988
1033
|
}
|
|
989
1034
|
if (isRequired) {
|
|
990
|
-
objectLines.push(` ${
|
|
1035
|
+
objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
|
|
991
1036
|
}
|
|
992
1037
|
else {
|
|
993
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
1038
|
+
objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${valueExpr} }),`);
|
|
994
1039
|
}
|
|
995
1040
|
}
|
|
996
1041
|
return objectLines;
|
|
@@ -1032,15 +1077,15 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
1032
1077
|
for (const { key, varName, isRequired, propSchema } of propInfo) {
|
|
1033
1078
|
if (!isSchemaObject(propSchema)) {
|
|
1034
1079
|
if (isRequired)
|
|
1035
|
-
fields.push(` ${
|
|
1080
|
+
fields.push(` ${safeLiteralKey(key)}: undefined,`);
|
|
1036
1081
|
continue;
|
|
1037
1082
|
}
|
|
1038
1083
|
const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports)
|
|
1039
1084
|
? varName
|
|
1040
1085
|
: safeAccessor('input', key);
|
|
1041
1086
|
fields.push(isRequired
|
|
1042
|
-
? ` ${
|
|
1043
|
-
: ` ...(${accessor} !== undefined && { ${
|
|
1087
|
+
? ` ${safeLiteralKey(key)}: ${accessor},`
|
|
1088
|
+
: ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
|
|
1044
1089
|
}
|
|
1045
1090
|
lines.push(` if (${deepGuard}) return {`);
|
|
1046
1091
|
lines.push(fields.join('\n'));
|
|
@@ -1148,6 +1193,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
1148
1193
|
* its imported parser when ref imports are on), and every other key is rejected
|
|
1149
1194
|
* in strict mode or stripped in coerce mode — matching the interpreter.
|
|
1150
1195
|
*/
|
|
1196
|
+
/**
|
|
1197
|
+
* Emits a prototype-safe `result[key] = value` for the dynamic `for..in` copy
|
|
1198
|
+
* loops in the pattern-property parsers. A bare `result[key] = v` is a `[[Set]]`
|
|
1199
|
+
* — and for the key `"__proto__"` (own-enumerable when the input came from
|
|
1200
|
+
* `JSON.parse`) it invokes the inherited `Object.prototype` setter and reassigns
|
|
1201
|
+
* `result`'s prototype to attacker-controlled data, the same vector
|
|
1202
|
+
* `validateRecord` guards against. `"__proto__"` is the only own-enumerable key
|
|
1203
|
+
* whose name resolves to an accessor on `Object.prototype` (`constructor` et al.
|
|
1204
|
+
* are writable data properties, so a `[[Set]]` there just creates an own key),
|
|
1205
|
+
* so a single-keyed guard is sufficient. Each loop already runs a `.test(key)`
|
|
1206
|
+
* regex per iteration, which dominates the cost, so the extra `===` on the
|
|
1207
|
+
* common path is immeasurable. Assumes the target variable is named `result`,
|
|
1208
|
+
* as it is in every parser that calls this.
|
|
1209
|
+
*/
|
|
1210
|
+
const safeResultAssign = (keyVar, valueExpr) => `if (${keyVar} === "__proto__") { Object.defineProperty(result, ${keyVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); } else { (result as Record<string, unknown>)[${keyVar}] = ${valueExpr}; }`;
|
|
1151
1211
|
const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
|
|
1152
1212
|
const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
|
|
1153
1213
|
// Below the inline threshold a key === "a" || key === "b" chain skips declared
|
|
@@ -1160,7 +1220,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
|
|
|
1160
1220
|
if (refPattern) {
|
|
1161
1221
|
const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
|
|
1162
1222
|
loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
|
|
1163
|
-
loopLines.push(` (
|
|
1223
|
+
loopLines.push(` ${safeResultAssign('key', `${parserName}(input[key])`)}`);
|
|
1164
1224
|
loopLines.push(` continue;`);
|
|
1165
1225
|
loopLines.push(` }`);
|
|
1166
1226
|
}
|
|
@@ -1169,7 +1229,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
|
|
|
1169
1229
|
.map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
|
|
1170
1230
|
if (keepConditions.length > 0) {
|
|
1171
1231
|
loopLines.push(` if (${keepConditions.join(' || ')}) {`);
|
|
1172
|
-
loopLines.push(` (
|
|
1232
|
+
loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
|
|
1173
1233
|
loopLines.push(` continue;`);
|
|
1174
1234
|
loopLines.push(` }`);
|
|
1175
1235
|
}
|
|
@@ -1183,10 +1243,11 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
|
|
|
1183
1243
|
const resultBody = propertyLines.length > 0 ? `{\n${propertyLines.join('\n')}\n }` : '{}';
|
|
1184
1244
|
// Only the Set form needs a declaration; the inline === chain is stateless.
|
|
1185
1245
|
const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};\n`).join('');
|
|
1246
|
+
const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
|
|
1186
1247
|
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
1187
1248
|
if (!isObject(input)) {
|
|
1188
1249
|
${notObjectBranch}
|
|
1189
|
-
}
|
|
1250
|
+
}${keywordPrelude}
|
|
1190
1251
|
${knownKeysDeclaration} const result = ${resultBody} as unknown as ${typeName};
|
|
1191
1252
|
for (const key in input) {
|
|
1192
1253
|
${loopLines.join('\n')}
|
|
@@ -1206,7 +1267,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
|
|
|
1206
1267
|
if (entry.isOptional) {
|
|
1207
1268
|
return ` ${entry.value},`;
|
|
1208
1269
|
}
|
|
1209
|
-
return ` ${
|
|
1270
|
+
return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
|
|
1210
1271
|
});
|
|
1211
1272
|
// Find the first pattern with a $ref for parser delegation
|
|
1212
1273
|
if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
|
|
@@ -1241,10 +1302,11 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
|
|
|
1241
1302
|
const notObjectBranch = strict
|
|
1242
1303
|
? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
|
|
1243
1304
|
: ` return {} as unknown as ${typeName};`;
|
|
1305
|
+
const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
|
|
1244
1306
|
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
1245
1307
|
if (!isObject(input)) {
|
|
1246
1308
|
${notObjectBranch}
|
|
1247
|
-
}
|
|
1309
|
+
}${keywordPrelude}
|
|
1248
1310
|
const result = {
|
|
1249
1311
|
${objectProperties}
|
|
1250
1312
|
} as unknown as ${typeName};
|
|
@@ -1265,14 +1327,15 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
|
|
|
1265
1327
|
const additionalProps = schema.additionalProperties;
|
|
1266
1328
|
// Check if additionalProps is defined before using it
|
|
1267
1329
|
if (!additionalProps) {
|
|
1268
|
-
return generateEmptyObjectParser(typeName, strict);
|
|
1330
|
+
return generateEmptyObjectParser(typeName, strict, schema);
|
|
1269
1331
|
}
|
|
1332
|
+
const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
|
|
1270
1333
|
// If additionalProperties is a $ref and useRefImports is true, generate a loop
|
|
1271
1334
|
if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
|
|
1272
1335
|
const ref = additionalProps.$ref;
|
|
1273
1336
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
1274
1337
|
if (strict) {
|
|
1275
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`)
|
|
1338
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return validateRecord(input, ${parserName}) as ${typeName};\n};`;
|
|
1276
1339
|
}
|
|
1277
1340
|
return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`;
|
|
1278
1341
|
}
|
|
@@ -1281,14 +1344,14 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
|
|
|
1281
1344
|
const inlineParser = generateInlineValueParser(additionalProps);
|
|
1282
1345
|
if (inlineParser) {
|
|
1283
1346
|
if (strict) {
|
|
1284
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`)
|
|
1347
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return validateRecord(input, ${inlineParser}) as ${typeName};\n};`;
|
|
1285
1348
|
}
|
|
1286
1349
|
return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`;
|
|
1287
1350
|
}
|
|
1288
1351
|
}
|
|
1289
1352
|
// Otherwise, just validate the input type and shallow copy
|
|
1290
1353
|
if (strict) {
|
|
1291
|
-
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`)
|
|
1354
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return { ...input } as ${typeName};\n};`;
|
|
1292
1355
|
}
|
|
1293
1356
|
return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`;
|
|
1294
1357
|
};
|
|
@@ -1307,8 +1370,11 @@ const generateInlineValueParser = (schema) => {
|
|
|
1307
1370
|
case 'string':
|
|
1308
1371
|
return '(value: unknown) => typeof value === "string" ? value : ""';
|
|
1309
1372
|
case 'number':
|
|
1310
|
-
case 'integer':
|
|
1311
1373
|
return '(value: unknown) => typeof value === "number" ? value : 0';
|
|
1374
|
+
case 'integer':
|
|
1375
|
+
// `integer` rejects non-integral numbers; a bare typeof accepts `1.5`,
|
|
1376
|
+
// matching every other integer site in this package (and strict mode).
|
|
1377
|
+
return '(value: unknown) => typeof value === "number" && Number.isInteger(value) ? value : 0';
|
|
1312
1378
|
case 'boolean':
|
|
1313
1379
|
return '(value: unknown) => typeof value === "boolean" ? value : false';
|
|
1314
1380
|
case 'array':
|
|
@@ -1324,47 +1390,53 @@ const generateInlineValueParser = (schema) => {
|
|
|
1324
1390
|
const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
|
|
1325
1391
|
const functionName = generateParserName(typeName);
|
|
1326
1392
|
if (!('patternProperties' in schema) || typeof schema.patternProperties !== 'object') {
|
|
1327
|
-
return generateEmptyObjectParser(typeName, strict);
|
|
1393
|
+
return generateEmptyObjectParser(typeName, strict, schema);
|
|
1328
1394
|
}
|
|
1329
1395
|
const patternProps = schema.patternProperties;
|
|
1330
1396
|
// Find the first pattern and its schema
|
|
1331
1397
|
const patterns = Object.entries(patternProps);
|
|
1332
1398
|
if (patterns.length === 0) {
|
|
1333
|
-
return generateEmptyObjectParser(typeName, strict);
|
|
1399
|
+
return generateEmptyObjectParser(typeName, strict, schema);
|
|
1334
1400
|
}
|
|
1335
1401
|
const [pattern, patternSchema] = patterns[0];
|
|
1336
1402
|
let patternAssignment = '(result as Record<string, unknown>)[key] = value;';
|
|
1403
|
+
// The value-expression form, used by the strictKeys path (which builds from
|
|
1404
|
+
// `{}`) to emit a prototype-safe assignment. `null` when no value is allowed.
|
|
1405
|
+
let patternValueExpr = 'value';
|
|
1337
1406
|
// Use imported parser when pattern schema points to a $ref.
|
|
1338
1407
|
if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
|
|
1339
1408
|
const ref = patternSchema.$ref;
|
|
1340
1409
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
1341
1410
|
patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
|
|
1411
|
+
patternValueExpr = `${parserName}(value)`;
|
|
1342
1412
|
}
|
|
1343
1413
|
else if (patternSchema === false) {
|
|
1344
1414
|
// `false` means no values are allowed for matching keys.
|
|
1345
1415
|
patternAssignment = '';
|
|
1416
|
+
patternValueExpr = null;
|
|
1346
1417
|
}
|
|
1347
1418
|
// Escape the pattern for safe inclusion in generated code
|
|
1348
1419
|
const escapedPattern = escapeRegexPattern(pattern);
|
|
1349
1420
|
const notObjectBranch = strict
|
|
1350
1421
|
? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
|
|
1351
1422
|
: ` return {} as unknown as ${typeName};`;
|
|
1423
|
+
const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
|
|
1352
1424
|
// With `additionalProperties: false`, only keys matching a pattern survive:
|
|
1353
1425
|
// others are rejected (strict) or stripped (coerce). Start from an empty
|
|
1354
1426
|
// object rather than spreading every input key.
|
|
1355
1427
|
const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
|
|
1356
1428
|
if (strictKeys) {
|
|
1357
1429
|
const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
|
|
1358
|
-
if (
|
|
1430
|
+
if (patternValueExpr) {
|
|
1359
1431
|
loopLines.push(` const value = input[key];`);
|
|
1360
|
-
loopLines.push(` ${
|
|
1432
|
+
loopLines.push(` ${safeResultAssign('key', patternValueExpr)}`);
|
|
1361
1433
|
}
|
|
1362
1434
|
loopLines.push(` continue;`);
|
|
1363
1435
|
loopLines.push(` }`);
|
|
1364
1436
|
const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
|
|
1365
1437
|
if (keepConditions.length > 0) {
|
|
1366
1438
|
loopLines.push(` if (${keepConditions.join(' || ')}) {`);
|
|
1367
|
-
loopLines.push(` (
|
|
1439
|
+
loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
|
|
1368
1440
|
loopLines.push(` continue;`);
|
|
1369
1441
|
loopLines.push(` }`);
|
|
1370
1442
|
}
|
|
@@ -1373,7 +1445,7 @@ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix
|
|
|
1373
1445
|
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
1374
1446
|
if (!isObject(input)) {
|
|
1375
1447
|
${notObjectBranch}
|
|
1376
|
-
}
|
|
1448
|
+
}${keywordPrelude}
|
|
1377
1449
|
const result = {} as unknown as ${typeName};
|
|
1378
1450
|
for (const key in input) {
|
|
1379
1451
|
${loopLines.join('\n')}
|
|
@@ -1385,7 +1457,7 @@ ${loopLines.join('\n')}
|
|
|
1385
1457
|
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
1386
1458
|
if (!isObject(input)) {
|
|
1387
1459
|
${notObjectBranch}
|
|
1388
|
-
}
|
|
1460
|
+
}${keywordPrelude}
|
|
1389
1461
|
const result = {
|
|
1390
1462
|
...input,
|
|
1391
1463
|
} as unknown as ${typeName};
|
|
@@ -1597,17 +1669,17 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1597
1669
|
const additionalProps = schema.additionalProperties;
|
|
1598
1670
|
// If additionalProperties is true or false, validate it is an object
|
|
1599
1671
|
if (additionalProps === true || additionalProps === false) {
|
|
1600
|
-
return generateEmptyObjectParser(typeName, strict);
|
|
1672
|
+
return generateEmptyObjectParser(typeName, strict, schema);
|
|
1601
1673
|
}
|
|
1602
1674
|
// Otherwise, handle as a schema (could be a $ref or object schema)
|
|
1603
1675
|
return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
|
|
1604
1676
|
}
|
|
1605
1677
|
// Handle empty object schemas
|
|
1606
1678
|
if ('type' in schema && schema.type === 'object' && !hasProperties(schema)) {
|
|
1607
|
-
return generateEmptyObjectParser(typeName, strict);
|
|
1679
|
+
return generateEmptyObjectParser(typeName, strict, schema);
|
|
1608
1680
|
}
|
|
1609
1681
|
// Default fallback - validate it is an object since we passed the isObjectSchema check
|
|
1610
|
-
return generateEmptyObjectParser(typeName, strict);
|
|
1682
|
+
return generateEmptyObjectParser(typeName, strict, schema);
|
|
1611
1683
|
};
|
|
1612
1684
|
/**
|
|
1613
1685
|
* Generates a safe parser as an arrow function that never throws.
|
|
@@ -1619,6 +1691,12 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1619
1691
|
* $ref will map each element through the imported parser.
|
|
1620
1692
|
*/
|
|
1621
1693
|
export const generateParserFunction = (schema, typeName, options) => {
|
|
1694
|
+
// Strict parsers promise to throw on violations, so any keyword we cannot
|
|
1695
|
+
// enforce must fail loudly at generation rather than emit a permissive parser.
|
|
1696
|
+
// Coercing parsers are documented to repair rather than reject, so the guard
|
|
1697
|
+
// does not apply to them.
|
|
1698
|
+
if (options?.strict)
|
|
1699
|
+
assertNoUnsupportedKeywords(schema, typeName);
|
|
1622
1700
|
return selectParserStrategy(schema, typeName, options);
|
|
1623
1701
|
};
|
|
1624
1702
|
/**
|
|
@@ -1645,6 +1723,12 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
|
|
|
1645
1723
|
const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
|
|
1646
1724
|
if (!isSchemaObject(schema))
|
|
1647
1725
|
return stub;
|
|
1726
|
+
// Keywords the parser's fast path cannot mirror (contains, dependentRequired,
|
|
1727
|
+
// dependentSchemas, propertyNames) make a bare shape match insufficient: a
|
|
1728
|
+
// parent that fast-pathed through this predicate would skip their enforcement,
|
|
1729
|
+
// so stub out and force the parent's slow path (which calls the real parser).
|
|
1730
|
+
if (hasFastPathBlockingKeyword(schema))
|
|
1731
|
+
return stub;
|
|
1648
1732
|
// An alias definition (a bare `$ref` with no shape of its own) delegates to
|
|
1649
1733
|
// the referenced type's validator. Guarded against self-reference.
|
|
1650
1734
|
if (useRefImports && hasRef(schema) && !hasProperties(schema)) {
|