@amritk/generate-parsers 0.8.0 → 0.9.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.
|
@@ -2,6 +2,7 @@ import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
|
|
|
2
2
|
import { refToName } from '@amritk/helpers/ref-to-name';
|
|
3
3
|
import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor';
|
|
4
4
|
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
5
|
+
import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
|
|
5
6
|
import { getDefaultValue } from '../helpers/get-default-value.js';
|
|
6
7
|
import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
|
|
7
8
|
import { generateValidationExpression } from './generate-validation-expression.js';
|
|
@@ -551,13 +552,17 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
551
552
|
preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false));
|
|
552
553
|
preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false));
|
|
553
554
|
}
|
|
554
|
-
// additionalProperties: false —
|
|
555
|
-
//
|
|
556
|
-
//
|
|
555
|
+
// additionalProperties: false — the per-key "is this undeclared" test inlines
|
|
556
|
+
// `!==` comparisons for a short key list and hoists a Set only for a long one
|
|
557
|
+
// (see unknownKeyCheck). The predicate form is shared by the fast path and the
|
|
558
|
+
// shape validator; strict mode throws with the offending key.
|
|
557
559
|
const strictKeys = hasStrictKeys(schema);
|
|
560
|
+
const strictKeyCheck = unknownKeyCheck(properties.map(([key]) => key), `_knownKeys${typeName}`);
|
|
558
561
|
if (strictKeys) {
|
|
559
|
-
|
|
560
|
-
|
|
562
|
+
for (const declaration of strictKeyCheck.declarations) {
|
|
563
|
+
preamble.push(`${declaration};`);
|
|
564
|
+
}
|
|
565
|
+
preamble.push(`const _hasOnlyKnownKeys${typeName} = (input: Record<string, unknown>): boolean => {\n for (const _k in input) if (${strictKeyCheck.isUnknown('_k')}) return false;\n return true;\n};`);
|
|
561
566
|
}
|
|
562
567
|
const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix, subTypeNames);
|
|
563
568
|
const lines = [];
|
|
@@ -568,7 +573,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
568
573
|
}
|
|
569
574
|
if (strictKeys) {
|
|
570
575
|
lines.push(` for (const _k in input) {`);
|
|
571
|
-
lines.push(` if (
|
|
576
|
+
lines.push(` if (${strictKeyCheck.isUnknown('_k')}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
|
|
572
577
|
lines.push(` }`);
|
|
573
578
|
}
|
|
574
579
|
}
|
|
@@ -631,10 +636,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
631
636
|
}
|
|
632
637
|
// Emit a warning for any input key not declared in the schema's properties
|
|
633
638
|
if (logWarnings && propInfo.length > 0) {
|
|
634
|
-
const
|
|
635
|
-
|
|
639
|
+
const warnKeyCheck = unknownKeyCheck(propInfo.map(({ key }) => key), '_knownKeys');
|
|
640
|
+
for (const declaration of warnKeyCheck.declarations) {
|
|
641
|
+
lines.push(` ${declaration};`);
|
|
642
|
+
}
|
|
636
643
|
lines.push(` for (const _k in input) {`);
|
|
637
|
-
lines.push(` if (
|
|
644
|
+
lines.push(` if (${warnKeyCheck.isUnknown('_k')}) {`);
|
|
638
645
|
lines.push(` console.warn(\`[${typeName}] Unknown property "\${_k}"\`);`);
|
|
639
646
|
lines.push(` }`);
|
|
640
647
|
lines.push(` }`);
|
|
@@ -747,6 +754,59 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
747
754
|
return fn;
|
|
748
755
|
return `${preamble.join('\n\n')}\n\n${fn}`;
|
|
749
756
|
};
|
|
757
|
+
/**
|
|
758
|
+
* Combined-parser variant for `properties` + `patternProperties` schemas that
|
|
759
|
+
* set `additionalProperties: false`. Declared properties are coerced as usual,
|
|
760
|
+
* keys matching any pattern are kept (the first `$ref` pattern is coerced via
|
|
761
|
+
* its imported parser when ref imports are on), and every other key is rejected
|
|
762
|
+
* in strict mode or stripped in coerce mode — matching the interpreter.
|
|
763
|
+
*/
|
|
764
|
+
const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
|
|
765
|
+
const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
|
|
766
|
+
// Below the inline threshold a key === "a" || key === "b" chain skips declared
|
|
767
|
+
// keys without the per-call Set allocation; a wider list hoists the Set.
|
|
768
|
+
const knownKeyCheck = unknownKeyCheck(declaredKeys, '_knownKeys');
|
|
769
|
+
// The first $ref pattern (when ref imports are on) coerces its matching values
|
|
770
|
+
// through the imported parser; the remaining patterns keep the raw value.
|
|
771
|
+
const refPattern = useRefImports ? patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps)) : undefined;
|
|
772
|
+
const loopLines = [` if (${knownKeyCheck.isKnown('key')}) continue;`];
|
|
773
|
+
if (refPattern) {
|
|
774
|
+
const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
|
|
775
|
+
loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
|
|
776
|
+
loopLines.push(` (result as Record<string, unknown>)[key] = ${parserName}(input[key]);`);
|
|
777
|
+
loopLines.push(` continue;`);
|
|
778
|
+
loopLines.push(` }`);
|
|
779
|
+
}
|
|
780
|
+
const keepConditions = patterns
|
|
781
|
+
.filter(([p]) => !(refPattern && p === refPattern[0]))
|
|
782
|
+
.map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
|
|
783
|
+
if (keepConditions.length > 0) {
|
|
784
|
+
loopLines.push(` if (${keepConditions.join(' || ')}) {`);
|
|
785
|
+
loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
|
|
786
|
+
loopLines.push(` continue;`);
|
|
787
|
+
loopLines.push(` }`);
|
|
788
|
+
}
|
|
789
|
+
// Unknown key: throw in strict mode, otherwise let it fall through (dropped,
|
|
790
|
+
// since it was never copied into `result`).
|
|
791
|
+
if (strict)
|
|
792
|
+
loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
|
|
793
|
+
const notObjectBranch = strict
|
|
794
|
+
? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
|
|
795
|
+
: ` return {} as unknown as ${typeName};`;
|
|
796
|
+
const resultBody = propertyLines.length > 0 ? `{\n${propertyLines.join('\n')}\n }` : '{}';
|
|
797
|
+
// Only the Set form needs a declaration; the inline === chain is stateless.
|
|
798
|
+
const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};\n`).join('');
|
|
799
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
800
|
+
if (!isObject(input)) {
|
|
801
|
+
${notObjectBranch}
|
|
802
|
+
}
|
|
803
|
+
${knownKeysDeclaration} const result = ${resultBody} as unknown as ${typeName};
|
|
804
|
+
for (const key in input) {
|
|
805
|
+
${loopLines.join('\n')}
|
|
806
|
+
}
|
|
807
|
+
return result;
|
|
808
|
+
};`;
|
|
809
|
+
};
|
|
750
810
|
/**
|
|
751
811
|
* Generates a parser for schemas that have both properties AND patternProperties.
|
|
752
812
|
* Parses the known properties first, then iterates remaining keys to match patterns.
|
|
@@ -768,6 +828,14 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
|
|
|
768
828
|
const patternProps = schema.patternProperties;
|
|
769
829
|
const patterns = Object.entries(patternProps);
|
|
770
830
|
const refPattern = patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps));
|
|
831
|
+
const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
|
|
832
|
+
// With `additionalProperties: false`, keys matching neither a declared
|
|
833
|
+
// property nor any pattern must be rejected (strict) or stripped (coerce).
|
|
834
|
+
// The blanket `...input` spread used below cannot do that, so build a
|
|
835
|
+
// selective copy that mirrors the interpreter's undeclared-key handling.
|
|
836
|
+
if (strictKeys) {
|
|
837
|
+
return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
|
|
838
|
+
}
|
|
771
839
|
if (!refPattern || !useRefImports) {
|
|
772
840
|
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict);
|
|
773
841
|
}
|
|
@@ -894,6 +962,38 @@ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix
|
|
|
894
962
|
const notObjectBranch = strict
|
|
895
963
|
? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
|
|
896
964
|
: ` return {} as unknown as ${typeName};`;
|
|
965
|
+
// With `additionalProperties: false`, only keys matching a pattern survive:
|
|
966
|
+
// others are rejected (strict) or stripped (coerce). Start from an empty
|
|
967
|
+
// object rather than spreading every input key.
|
|
968
|
+
const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
|
|
969
|
+
if (strictKeys) {
|
|
970
|
+
const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
|
|
971
|
+
if (patternAssignment) {
|
|
972
|
+
loopLines.push(` const value = input[key];`);
|
|
973
|
+
loopLines.push(` ${patternAssignment}`);
|
|
974
|
+
}
|
|
975
|
+
loopLines.push(` continue;`);
|
|
976
|
+
loopLines.push(` }`);
|
|
977
|
+
const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
|
|
978
|
+
if (keepConditions.length > 0) {
|
|
979
|
+
loopLines.push(` if (${keepConditions.join(' || ')}) {`);
|
|
980
|
+
loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
|
|
981
|
+
loopLines.push(` continue;`);
|
|
982
|
+
loopLines.push(` }`);
|
|
983
|
+
}
|
|
984
|
+
if (strict)
|
|
985
|
+
loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
|
|
986
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
987
|
+
if (!isObject(input)) {
|
|
988
|
+
${notObjectBranch}
|
|
989
|
+
}
|
|
990
|
+
const result = {} as unknown as ${typeName};
|
|
991
|
+
for (const key in input) {
|
|
992
|
+
${loopLines.join('\n')}
|
|
993
|
+
}
|
|
994
|
+
return result;
|
|
995
|
+
};`;
|
|
996
|
+
}
|
|
897
997
|
// Generate a parser that handles both pattern matching and x- extensions
|
|
898
998
|
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
899
999
|
if (!isObject(input)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-parsers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"json-schema-typed": "^8.0.1",
|
|
50
|
-
"@amritk/helpers": "0.
|
|
50
|
+
"@amritk/helpers": "0.10.0",
|
|
51
51
|
"@amritk/generate-markdown": "0.2.4"
|
|
52
52
|
}
|
|
53
53
|
}
|