@amritk/generate-validators 0.3.1 → 0.4.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/dist/generators/build-schema.d.ts +1 -1
- package/dist/generators/collect-validator-imports.d.ts +6 -1
- package/dist/generators/generate-files.d.ts +5 -0
- package/dist/generators/generate-validator-function.d.ts +1 -1
- package/dist/index.js +71 -39
- package/package.json +2 -2
- package/src/generators/build-schema.test.ts +2 -2
- package/src/generators/build-schema.ts +8 -2
- package/src/generators/collect-validator-imports.ts +10 -4
- package/src/generators/generate-files.ts +9 -2
- package/src/generators/generate-validator-function.test.ts +1 -1
- package/src/generators/generate-validator-function.ts +12 -12
|
@@ -27,4 +27,4 @@ export type GeneratedFile = {
|
|
|
27
27
|
* // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
export declare const buildValidatorSchema: (rootSchema: JSONSchema, rootTypeName: string) => Promise<GeneratedFile[]>;
|
|
30
|
+
export declare const buildValidatorSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>;
|
|
@@ -13,6 +13,11 @@ type CollectValidatorImportsOptions = {
|
|
|
13
13
|
* are excluded from the import list (they were never generated as files).
|
|
14
14
|
*/
|
|
15
15
|
readonly rootSchema?: Record<string, unknown> | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Suffix appended to every type/validator name derived from a `$ref`. Must
|
|
18
|
+
* match the suffix used when generating the referenced files. Defaults to `''`.
|
|
19
|
+
*/
|
|
20
|
+
readonly typeSuffix?: string;
|
|
16
21
|
};
|
|
17
22
|
/**
|
|
18
23
|
* Collects import statements for all $ref dependencies of a schema.
|
|
@@ -22,7 +27,7 @@ type CollectValidatorImportsOptions = {
|
|
|
22
27
|
* ```typescript
|
|
23
28
|
* const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
|
|
24
29
|
* collectValidatorImports(schema)
|
|
25
|
-
* // ["import { type
|
|
30
|
+
* // ["import { type Contact, validateContact } from './contact'"]
|
|
26
31
|
* ```
|
|
27
32
|
*/
|
|
28
33
|
export declare const collectValidatorImports: (schema: JSONSchema, options?: CollectValidatorImportsOptions) => string[];
|
|
@@ -12,6 +12,11 @@ type GenerateValidatorFileOptions = {
|
|
|
12
12
|
* The root schema document. Used to filter out unresolvable refs.
|
|
13
13
|
*/
|
|
14
14
|
readonly rootSchema?: Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Suffix appended to every type/validator name derived from a `$ref`.
|
|
17
|
+
* Defaults to `''` (no suffix).
|
|
18
|
+
*/
|
|
19
|
+
readonly typeSuffix?: string;
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
22
|
* Generates a complete TypeScript validator file from a JSON Schema.
|
|
@@ -21,4 +21,4 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
|
21
21
|
* // }
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export declare const generateValidatorFunction: (schema: JSONSchema, typeName: string) => string;
|
|
24
|
+
export declare const generateValidatorFunction: (schema: JSONSchema, typeName: string, suffix?: string) => string;
|
package/dist/index.js
CHANGED
|
@@ -132,15 +132,15 @@ var refToFilename2 = (ref) => {
|
|
|
132
132
|
}
|
|
133
133
|
return filename;
|
|
134
134
|
};
|
|
135
|
-
var kebabToPascal = (kebab) => {
|
|
135
|
+
var kebabToPascal = (kebab, suffix) => {
|
|
136
136
|
const words = kebab.split("-");
|
|
137
137
|
let pascalCase = "";
|
|
138
138
|
for (const word of words) {
|
|
139
139
|
pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
|
|
140
140
|
}
|
|
141
|
-
return pascalCase +
|
|
141
|
+
return pascalCase + suffix;
|
|
142
142
|
};
|
|
143
|
-
var refToName = (ref) => kebabToPascal(refToFilename2(ref));
|
|
143
|
+
var refToName = (ref, suffix = "") => kebabToPascal(refToFilename2(ref), suffix);
|
|
144
144
|
|
|
145
145
|
// ../helpers/dist/resolve-dynamic-refs.js
|
|
146
146
|
var resolveDynamicRefs = (schema, dynamicRefMap) => {
|
|
@@ -415,15 +415,15 @@ var refToFilename4 = (ref) => {
|
|
|
415
415
|
}
|
|
416
416
|
return filename;
|
|
417
417
|
};
|
|
418
|
-
var kebabToPascal2 = (kebab) => {
|
|
418
|
+
var kebabToPascal2 = (kebab, suffix) => {
|
|
419
419
|
const words = kebab.split("-");
|
|
420
420
|
let pascalCase = "";
|
|
421
421
|
for (const word of words) {
|
|
422
422
|
pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
|
|
423
423
|
}
|
|
424
|
-
return pascalCase +
|
|
424
|
+
return pascalCase + suffix;
|
|
425
425
|
};
|
|
426
|
-
var refToName2 = (ref) => kebabToPascal2(refToFilename4(ref));
|
|
426
|
+
var refToName2 = (ref, suffix = "") => kebabToPascal2(refToFilename4(ref), suffix);
|
|
427
427
|
var JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
428
428
|
var safeKey = (key) => {
|
|
429
429
|
if (JS_IDENTIFIER.test(key)) {
|
|
@@ -541,13 +541,13 @@ var getUnbrandedType = (schema, options = {}) => {
|
|
|
541
541
|
if (!schema.$ref.startsWith("#")) {
|
|
542
542
|
return "unknown";
|
|
543
543
|
}
|
|
544
|
-
return refToName2(schema.$ref);
|
|
544
|
+
return refToName2(schema.$ref, options.typeSuffix);
|
|
545
545
|
}
|
|
546
546
|
if (schema.$dynamicRef) {
|
|
547
547
|
if (schema.$dynamicRef === "#meta") {
|
|
548
|
-
return
|
|
548
|
+
return `Schema${options.typeSuffix ?? ""}`;
|
|
549
549
|
}
|
|
550
|
-
return refToName2(schema.$dynamicRef);
|
|
550
|
+
return refToName2(schema.$dynamicRef, options.typeSuffix);
|
|
551
551
|
}
|
|
552
552
|
if (schema.const !== undefined) {
|
|
553
553
|
return JSON.stringify(schema.const);
|
|
@@ -594,7 +594,7 @@ var getUnbrandedType = (schema, options = {}) => {
|
|
|
594
594
|
if (conditionalResult) {
|
|
595
595
|
const baseType = getTypeScriptType(conditionalResult.schema, options);
|
|
596
596
|
if (conditionalResult.thenRef) {
|
|
597
|
-
return `(${baseType}) & ${refToName2(conditionalResult.thenRef)}`;
|
|
597
|
+
return `(${baseType}) & ${refToName2(conditionalResult.thenRef, options.typeSuffix)}`;
|
|
598
598
|
}
|
|
599
599
|
return baseType;
|
|
600
600
|
}
|
|
@@ -675,6 +675,31 @@ var getUnbrandedType = (schema, options = {}) => {
|
|
|
675
675
|
case "object":
|
|
676
676
|
if (schema.properties) {
|
|
677
677
|
const readonlyPrefix = options.readonly ? "readonly " : "";
|
|
678
|
+
const hasDescriptions = Object.values(schema.properties).some((p) => isSchemaObject2(p) && (typeof p.description === "string" || typeof p.$comment === "string"));
|
|
679
|
+
if (hasDescriptions) {
|
|
680
|
+
let properties2 = "";
|
|
681
|
+
let first2 = true;
|
|
682
|
+
for (const key in schema.properties) {
|
|
683
|
+
const propSchema = schema.properties[key];
|
|
684
|
+
const isRequired = schema.required?.includes(key) ?? false;
|
|
685
|
+
const optional = isRequired ? "" : "?";
|
|
686
|
+
const propType = getTypeScriptType(propSchema, options);
|
|
687
|
+
const inlineDescription = isSchemaObject2(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject2(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined;
|
|
688
|
+
if (!first2)
|
|
689
|
+
properties2 += `
|
|
690
|
+
`;
|
|
691
|
+
first2 = false;
|
|
692
|
+
if (inlineDescription) {
|
|
693
|
+
properties2 += " /** " + inlineDescription + ` */
|
|
694
|
+
` + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";";
|
|
695
|
+
} else {
|
|
696
|
+
properties2 += " " + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";";
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return `{
|
|
700
|
+
` + properties2 + `
|
|
701
|
+
}`;
|
|
702
|
+
}
|
|
678
703
|
let properties = "";
|
|
679
704
|
let first = true;
|
|
680
705
|
for (const key in schema.properties) {
|
|
@@ -716,8 +741,9 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
|
|
|
716
741
|
if (!isObjectLikeSchema(schema)) {
|
|
717
742
|
const tsType = getTypeScriptType(schema, options);
|
|
718
743
|
let result = "";
|
|
719
|
-
|
|
720
|
-
|
|
744
|
+
const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined;
|
|
745
|
+
if (topLevelComment) {
|
|
746
|
+
result += buildJsDocBlock(typeName, topLevelComment);
|
|
721
747
|
}
|
|
722
748
|
result += `export type ${typeName} = ${tsType};`;
|
|
723
749
|
return result;
|
|
@@ -728,9 +754,10 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
|
|
|
728
754
|
const conditionalThenRef = conditionalResult?.thenRef ?? null;
|
|
729
755
|
let jsDocTitle;
|
|
730
756
|
let jsDocDescription;
|
|
731
|
-
|
|
757
|
+
const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined;
|
|
758
|
+
if (topLevelComment) {
|
|
732
759
|
jsDocTitle = typeName;
|
|
733
|
-
jsDocDescription =
|
|
760
|
+
jsDocDescription = topLevelComment;
|
|
734
761
|
}
|
|
735
762
|
const hasProperties2 = normalizedSchema.properties && Object.keys(normalizedSchema.properties).length > 0;
|
|
736
763
|
const hasAdditionalProperties2 = normalizedSchema.additionalProperties && typeof normalizedSchema.additionalProperties === "object";
|
|
@@ -787,12 +814,12 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
|
|
|
787
814
|
if (isSchemaObject2(schema) && Array.isArray(schema.allOf)) {
|
|
788
815
|
for (const entry of schema.allOf) {
|
|
789
816
|
if (isSchemaObject2(entry) && entry.$ref) {
|
|
790
|
-
allOfIntersections.push(refToName2(entry.$ref));
|
|
817
|
+
allOfIntersections.push(refToName2(entry.$ref, options.typeSuffix));
|
|
791
818
|
}
|
|
792
819
|
}
|
|
793
820
|
}
|
|
794
821
|
if (isSchemaObject2(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#")) {
|
|
795
|
-
allOfIntersections.push(refToName2(schema.$ref));
|
|
822
|
+
allOfIntersections.push(refToName2(schema.$ref, options.typeSuffix));
|
|
796
823
|
}
|
|
797
824
|
let result = "";
|
|
798
825
|
if (jsDocTitle && jsDocDescription) {
|
|
@@ -802,7 +829,7 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
|
|
|
802
829
|
` + properties + `
|
|
803
830
|
}`;
|
|
804
831
|
if (conditionalThenRef) {
|
|
805
|
-
typeBody += " & " + refToName2(conditionalThenRef);
|
|
832
|
+
typeBody += " & " + refToName2(conditionalThenRef, options.typeSuffix);
|
|
806
833
|
}
|
|
807
834
|
for (const intersectionType of allOfIntersections) {
|
|
808
835
|
typeBody += " & " + intersectionType;
|
|
@@ -876,9 +903,9 @@ var hasMultipleOf = (schema) => {
|
|
|
876
903
|
};
|
|
877
904
|
|
|
878
905
|
// src/generators/collect-validator-imports.ts
|
|
879
|
-
var buildImport = (ref) => {
|
|
906
|
+
var buildImport = (ref, suffix) => {
|
|
880
907
|
const filename = refToFilename(ref);
|
|
881
|
-
const typeName = refToName(ref);
|
|
908
|
+
const typeName = refToName(ref, suffix);
|
|
882
909
|
const validatorName = `validate${typeName}`;
|
|
883
910
|
return `import { type ${typeName}, ${validatorName} } from './${filename}'`;
|
|
884
911
|
};
|
|
@@ -923,6 +950,7 @@ var collectDirectRefs = (schema) => {
|
|
|
923
950
|
var collectValidatorImports = (schema, options) => {
|
|
924
951
|
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
|
|
925
952
|
const rootSchema = options?.rootSchema;
|
|
953
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
926
954
|
const refs = collectDirectRefs(schema);
|
|
927
955
|
const seen = new Set;
|
|
928
956
|
const imports = [];
|
|
@@ -939,7 +967,7 @@ var collectValidatorImports = (schema, options) => {
|
|
|
939
967
|
}
|
|
940
968
|
seen.add(filename);
|
|
941
969
|
const importRef = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
|
|
942
|
-
imports.push(buildImport(importRef));
|
|
970
|
+
imports.push(buildImport(importRef, typeSuffix));
|
|
943
971
|
}
|
|
944
972
|
return imports;
|
|
945
973
|
};
|
|
@@ -993,7 +1021,7 @@ var wrongTypeCondition = (accessor, type) => {
|
|
|
993
1021
|
return "";
|
|
994
1022
|
}
|
|
995
1023
|
};
|
|
996
|
-
var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
1024
|
+
var generatePropertyChecks = (key, propSchema, isRequired, suffix) => {
|
|
997
1025
|
if (!isSchemaObject3(propSchema))
|
|
998
1026
|
return [];
|
|
999
1027
|
const raw = `obj[${JSON.stringify(key)}]`;
|
|
@@ -1001,7 +1029,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
|
1001
1029
|
const lines = [];
|
|
1002
1030
|
if (hasRef(propSchema)) {
|
|
1003
1031
|
const ref = propSchema.$ref;
|
|
1004
|
-
const vName = validatorName(refToName(ref));
|
|
1032
|
+
const vName = validatorName(refToName(ref, suffix));
|
|
1005
1033
|
if (isRequired) {
|
|
1006
1034
|
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
|
|
1007
1035
|
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
|
|
@@ -1127,7 +1155,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
|
1127
1155
|
if (t === "array" && hasItems(propSchema)) {
|
|
1128
1156
|
const itemSchema = propSchema.items;
|
|
1129
1157
|
if (hasRef(itemSchema)) {
|
|
1130
|
-
const vName = validatorName(refToName(itemSchema.$ref));
|
|
1158
|
+
const vName = validatorName(refToName(itemSchema.$ref, suffix));
|
|
1131
1159
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
1132
1160
|
lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
|
|
1133
1161
|
lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`);
|
|
@@ -1151,19 +1179,19 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
|
1151
1179
|
}
|
|
1152
1180
|
return lines;
|
|
1153
1181
|
};
|
|
1154
|
-
var generateObjectValidator = (schema, typeName) => {
|
|
1182
|
+
var generateObjectValidator = (schema, typeName, suffix) => {
|
|
1155
1183
|
const vName = validatorName(typeName);
|
|
1156
1184
|
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
1157
1185
|
const properties = hasProperties(schema) ? schema.properties : {};
|
|
1158
1186
|
const propertyLines = [];
|
|
1159
1187
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
1160
|
-
const checks = generatePropertyChecks(key, propSchema, required.has(key));
|
|
1188
|
+
const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix);
|
|
1161
1189
|
if (checks.length > 0) {
|
|
1162
1190
|
propertyLines.push(...checks);
|
|
1163
1191
|
}
|
|
1164
1192
|
}
|
|
1165
1193
|
if (hasAdditionalProperties(schema) && isSchemaObject3(schema.additionalProperties) && hasRef(schema.additionalProperties)) {
|
|
1166
|
-
const vRefName = validatorName(refToName(schema.additionalProperties.$ref));
|
|
1194
|
+
const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix));
|
|
1167
1195
|
propertyLines.push(` for (const _key of Object.keys(obj)) {`);
|
|
1168
1196
|
propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`);
|
|
1169
1197
|
propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`);
|
|
@@ -1188,14 +1216,14 @@ var generateObjectValidator = (schema, typeName) => {
|
|
|
1188
1216
|
].join(`
|
|
1189
1217
|
`);
|
|
1190
1218
|
};
|
|
1191
|
-
var generateScalarValidator = (schema, typeName) => {
|
|
1219
|
+
var generateScalarValidator = (schema, typeName, suffix) => {
|
|
1192
1220
|
const vName = validatorName(typeName);
|
|
1193
1221
|
if (!isSchemaObject3(schema)) {
|
|
1194
1222
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
|
|
1195
1223
|
`);
|
|
1196
1224
|
}
|
|
1197
1225
|
if (hasRef(schema)) {
|
|
1198
|
-
const delegateName = validatorName(refToName(schema.$ref));
|
|
1226
|
+
const delegateName = validatorName(refToName(schema.$ref, suffix));
|
|
1199
1227
|
return [
|
|
1200
1228
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1201
1229
|
` return ${delegateName}(input, _path)`,
|
|
@@ -1244,7 +1272,7 @@ var generateScalarValidator = (schema, typeName) => {
|
|
|
1244
1272
|
const branches = schema.oneOf.map((branch, i) => {
|
|
1245
1273
|
if (!hasRef(branch))
|
|
1246
1274
|
return null;
|
|
1247
|
-
const bName = validatorName(refToName(branch.$ref));
|
|
1275
|
+
const bName = validatorName(refToName(branch.$ref, suffix));
|
|
1248
1276
|
return ` const _r${i} = ${bName}(input, _path)
|
|
1249
1277
|
if (_r${i} === true) return true`;
|
|
1250
1278
|
}).filter(Boolean).join(`
|
|
@@ -1314,21 +1342,23 @@ var generateScalarValidator = (schema, typeName) => {
|
|
|
1314
1342
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
|
|
1315
1343
|
`);
|
|
1316
1344
|
};
|
|
1317
|
-
var generateValidatorFunction = (schema, typeName) => {
|
|
1345
|
+
var generateValidatorFunction = (schema, typeName, suffix = "") => {
|
|
1318
1346
|
if (isObjectSchema2(schema)) {
|
|
1319
|
-
return generateObjectValidator(schema, typeName);
|
|
1347
|
+
return generateObjectValidator(schema, typeName, suffix);
|
|
1320
1348
|
}
|
|
1321
|
-
return generateScalarValidator(schema, typeName);
|
|
1349
|
+
return generateScalarValidator(schema, typeName, suffix);
|
|
1322
1350
|
};
|
|
1323
1351
|
|
|
1324
1352
|
// src/generators/generate-files.ts
|
|
1325
1353
|
var generateValidatorFile = (schema, typeName, options) => {
|
|
1354
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
1326
1355
|
const refImports = collectValidatorImports(schema, {
|
|
1327
1356
|
selfRef: options?.selfRef,
|
|
1328
|
-
rootSchema: options?.rootSchema
|
|
1357
|
+
rootSchema: options?.rootSchema,
|
|
1358
|
+
typeSuffix
|
|
1329
1359
|
});
|
|
1330
|
-
const typeDefinition = generateTypeDefinition(schema, typeName);
|
|
1331
|
-
const validatorFunction = generateValidatorFunction(schema, typeName);
|
|
1360
|
+
const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
|
|
1361
|
+
const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
|
|
1332
1362
|
let result = `import type { ValidationResult, ValidationError } from './validation-result'
|
|
1333
1363
|
`;
|
|
1334
1364
|
for (const imp of refImports) {
|
|
@@ -1365,7 +1395,7 @@ export type ValidationError = {
|
|
|
1365
1395
|
*/
|
|
1366
1396
|
export type ValidationResult = true | { valid: false; errors: ValidationError[] }
|
|
1367
1397
|
`;
|
|
1368
|
-
var buildValidatorSchema = async (rootSchema, rootTypeName) => {
|
|
1398
|
+
var buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "") => {
|
|
1369
1399
|
rootSchema = upgradeDraft07Schema(rootSchema);
|
|
1370
1400
|
const files = [];
|
|
1371
1401
|
const processedRefs = new Set;
|
|
@@ -1374,7 +1404,8 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
|
|
|
1374
1404
|
const dynamicRefMap = buildDynamicRefMap(rootSchema);
|
|
1375
1405
|
const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap);
|
|
1376
1406
|
const rootContent = generateValidatorFile(processedRootSchema, rootTypeName, {
|
|
1377
|
-
rootSchema
|
|
1407
|
+
rootSchema,
|
|
1408
|
+
typeSuffix
|
|
1378
1409
|
});
|
|
1379
1410
|
const rootFilename = rootTypeName.toLowerCase();
|
|
1380
1411
|
if (rootFilename !== "validation-result") {
|
|
@@ -1393,12 +1424,13 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
|
|
|
1393
1424
|
console.warn(`Warning: Could not resolve ref: ${ref}`);
|
|
1394
1425
|
continue;
|
|
1395
1426
|
}
|
|
1396
|
-
const typeName = refToName(ref);
|
|
1427
|
+
const typeName = refToName(ref, typeSuffix);
|
|
1397
1428
|
const filename = refToFilename(ref);
|
|
1398
1429
|
const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap);
|
|
1399
1430
|
const content = generateValidatorFile(processedSchema, typeName, {
|
|
1400
1431
|
selfRef: ref,
|
|
1401
|
-
rootSchema
|
|
1432
|
+
rootSchema,
|
|
1433
|
+
typeSuffix
|
|
1402
1434
|
});
|
|
1403
1435
|
if (filename !== "validation-result" && !processedFilenames.has(filename)) {
|
|
1404
1436
|
processedFilenames.add(filename);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-validators",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Generate TypeScript validation functions 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.6.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@scalar/openapi-parser": "^0.26.1"
|
|
@@ -74,7 +74,7 @@ describe('build-schema', () => {
|
|
|
74
74
|
const documentFile = files.find((f) => f.filename === 'document.ts')
|
|
75
75
|
|
|
76
76
|
expect(documentFile?.content).toContain("from './info'")
|
|
77
|
-
expect(documentFile?.content).toContain('
|
|
77
|
+
expect(documentFile?.content).toContain('validateInfo')
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
it('generates a valid index.ts with re-exports', async () => {
|
|
@@ -136,7 +136,7 @@ describe('build-schema', () => {
|
|
|
136
136
|
const infoFile = files.find((f) => f.filename === 'info.ts')
|
|
137
137
|
|
|
138
138
|
expect(documentFile?.content).toContain('validateDocument')
|
|
139
|
-
expect(infoFile?.content).toContain('
|
|
139
|
+
expect(infoFile?.content).toContain('validateInfo')
|
|
140
140
|
|
|
141
141
|
// Cross-check: @scalar/openapi-parser says a complete document is valid
|
|
142
142
|
const validDoc = { openapi: '3.1.0', info: { title: 'API', version: '1.0' }, paths: {} }
|
|
@@ -55,7 +55,11 @@ export type ValidationResult = true | { valid: false; errors: ValidationError[]
|
|
|
55
55
|
* // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
|
|
56
56
|
* ```
|
|
57
57
|
*/
|
|
58
|
-
export const buildValidatorSchema = async (
|
|
58
|
+
export const buildValidatorSchema = async (
|
|
59
|
+
rootSchema: JSONSchema,
|
|
60
|
+
rootTypeName: string,
|
|
61
|
+
typeSuffix = '',
|
|
62
|
+
): Promise<GeneratedFile[]> => {
|
|
59
63
|
rootSchema = upgradeDraft07Schema(rootSchema as Record<string, unknown>) as JSONSchema
|
|
60
64
|
|
|
61
65
|
const files: GeneratedFile[] = []
|
|
@@ -69,6 +73,7 @@ export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName:
|
|
|
69
73
|
const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap)
|
|
70
74
|
const rootContent = generateValidatorFile(processedRootSchema, rootTypeName, {
|
|
71
75
|
rootSchema: rootSchema as Record<string, unknown>,
|
|
76
|
+
typeSuffix,
|
|
72
77
|
})
|
|
73
78
|
const rootFilename = rootTypeName.toLowerCase()
|
|
74
79
|
|
|
@@ -91,12 +96,13 @@ export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName:
|
|
|
91
96
|
continue
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
const typeName = refToName(ref)
|
|
99
|
+
const typeName = refToName(ref, typeSuffix)
|
|
95
100
|
const filename = refToFilename(ref)
|
|
96
101
|
const processedSchema = resolveDynamicRefs(resolvedSchema as JSONSchema, dynamicRefMap)
|
|
97
102
|
const content = generateValidatorFile(processedSchema, typeName, {
|
|
98
103
|
selfRef: ref,
|
|
99
104
|
rootSchema: rootSchema as Record<string, unknown>,
|
|
105
|
+
typeSuffix,
|
|
100
106
|
})
|
|
101
107
|
|
|
102
108
|
if (filename !== 'validation-result' && !processedFilenames.has(filename)) {
|
|
@@ -18,15 +18,20 @@ type CollectValidatorImportsOptions = {
|
|
|
18
18
|
* are excluded from the import list (they were never generated as files).
|
|
19
19
|
*/
|
|
20
20
|
readonly rootSchema?: Record<string, unknown> | undefined
|
|
21
|
+
/**
|
|
22
|
+
* Suffix appended to every type/validator name derived from a `$ref`. Must
|
|
23
|
+
* match the suffix used when generating the referenced files. Defaults to `''`.
|
|
24
|
+
*/
|
|
25
|
+
readonly typeSuffix?: string
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Generates an import statement for a single $ref, importing both the type
|
|
25
30
|
* and the validator function from the ref's generated file.
|
|
26
31
|
*/
|
|
27
|
-
const buildImport = (ref: string): string => {
|
|
32
|
+
const buildImport = (ref: string, suffix: string): string => {
|
|
28
33
|
const filename = refToFilename(ref)
|
|
29
|
-
const typeName = refToName(ref)
|
|
34
|
+
const typeName = refToName(ref, suffix)
|
|
30
35
|
const validatorName = `validate${typeName}`
|
|
31
36
|
return `import { type ${typeName}, ${validatorName} } from './${filename}'`
|
|
32
37
|
}
|
|
@@ -94,12 +99,13 @@ const collectDirectRefs = (schema: JSONSchema): string[] => {
|
|
|
94
99
|
* ```typescript
|
|
95
100
|
* const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
|
|
96
101
|
* collectValidatorImports(schema)
|
|
97
|
-
* // ["import { type
|
|
102
|
+
* // ["import { type Contact, validateContact } from './contact'"]
|
|
98
103
|
* ```
|
|
99
104
|
*/
|
|
100
105
|
export const collectValidatorImports = (schema: JSONSchema, options?: CollectValidatorImportsOptions): string[] => {
|
|
101
106
|
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null
|
|
102
107
|
const rootSchema = options?.rootSchema
|
|
108
|
+
const typeSuffix = options?.typeSuffix ?? ''
|
|
103
109
|
|
|
104
110
|
const refs = collectDirectRefs(schema)
|
|
105
111
|
const seen = new Set<string>()
|
|
@@ -121,7 +127,7 @@ export const collectValidatorImports = (schema: JSONSchema, options?: CollectVal
|
|
|
121
127
|
|
|
122
128
|
// -or-reference unions import the base type's validator
|
|
123
129
|
const importRef = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref
|
|
124
|
-
imports.push(buildImport(importRef))
|
|
130
|
+
imports.push(buildImport(importRef, typeSuffix))
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
return imports
|
|
@@ -17,6 +17,11 @@ type GenerateValidatorFileOptions = {
|
|
|
17
17
|
* The root schema document. Used to filter out unresolvable refs.
|
|
18
18
|
*/
|
|
19
19
|
readonly rootSchema?: Record<string, unknown>
|
|
20
|
+
/**
|
|
21
|
+
* Suffix appended to every type/validator name derived from a `$ref`.
|
|
22
|
+
* Defaults to `''` (no suffix).
|
|
23
|
+
*/
|
|
24
|
+
readonly typeSuffix?: string
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
/**
|
|
@@ -46,13 +51,15 @@ export const generateValidatorFile = (
|
|
|
46
51
|
typeName: string,
|
|
47
52
|
options?: GenerateValidatorFileOptions,
|
|
48
53
|
): string => {
|
|
54
|
+
const typeSuffix = options?.typeSuffix ?? ''
|
|
49
55
|
const refImports = collectValidatorImports(schema, {
|
|
50
56
|
selfRef: options?.selfRef,
|
|
51
57
|
rootSchema: options?.rootSchema,
|
|
58
|
+
typeSuffix,
|
|
52
59
|
})
|
|
53
60
|
|
|
54
|
-
const typeDefinition = generateTypeDefinition(schema, typeName)
|
|
55
|
-
const validatorFunction = generateValidatorFunction(schema, typeName)
|
|
61
|
+
const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix })
|
|
62
|
+
const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix)
|
|
56
63
|
|
|
57
64
|
let result = `import type { ValidationResult, ValidationError } from './validation-result'\n`
|
|
58
65
|
|
|
@@ -135,7 +135,7 @@ describe('generate-validator-function', () => {
|
|
|
135
135
|
}
|
|
136
136
|
const code = generateValidatorFunction(schema, 'Document')
|
|
137
137
|
|
|
138
|
-
expect(code).toContain('
|
|
138
|
+
expect(code).toContain('validateInfo(')
|
|
139
139
|
expect(code).toContain('"info" in obj')
|
|
140
140
|
})
|
|
141
141
|
|
|
@@ -61,7 +61,7 @@ const wrongTypeCondition = (accessor: string, type: string): string => {
|
|
|
61
61
|
* Generates validation lines for a single property in an object schema.
|
|
62
62
|
* Handles $ref delegation, enum checks, type checks, and string/number constraints.
|
|
63
63
|
*/
|
|
64
|
-
const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired: boolean): string[] => {
|
|
64
|
+
const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired: boolean, suffix: string): string[] => {
|
|
65
65
|
if (!isSchemaObject(propSchema)) return []
|
|
66
66
|
|
|
67
67
|
const raw = `obj[${JSON.stringify(key)}]`
|
|
@@ -71,7 +71,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
|
|
|
71
71
|
// $ref — delegate to the imported validator
|
|
72
72
|
if (hasRef(propSchema)) {
|
|
73
73
|
const ref = propSchema.$ref
|
|
74
|
-
const vName = validatorName(refToName(ref))
|
|
74
|
+
const vName = validatorName(refToName(ref, suffix))
|
|
75
75
|
|
|
76
76
|
if (isRequired) {
|
|
77
77
|
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
@@ -218,7 +218,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
|
|
|
218
218
|
if (t === 'array' && hasItems(propSchema)) {
|
|
219
219
|
const itemSchema = propSchema.items
|
|
220
220
|
if (hasRef(itemSchema)) {
|
|
221
|
-
const vName = validatorName(refToName(itemSchema.$ref))
|
|
221
|
+
const vName = validatorName(refToName(itemSchema.$ref, suffix))
|
|
222
222
|
lines.push(` if (Array.isArray(${raw})) {`)
|
|
223
223
|
lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`)
|
|
224
224
|
lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`)
|
|
@@ -250,7 +250,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
|
|
|
250
250
|
* Generates a validator function body for an object schema, checking each
|
|
251
251
|
* property's presence and type and collecting all errors.
|
|
252
252
|
*/
|
|
253
|
-
const generateObjectValidator = (schema: JSONSchema, typeName: string): string => {
|
|
253
|
+
const generateObjectValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
|
|
254
254
|
const vName = validatorName(typeName)
|
|
255
255
|
const required = new Set(hasRequired(schema) ? schema.required : [])
|
|
256
256
|
const properties = hasProperties(schema) ? schema.properties : {}
|
|
@@ -258,7 +258,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
258
258
|
const propertyLines: string[] = []
|
|
259
259
|
|
|
260
260
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
261
|
-
const checks = generatePropertyChecks(key, propSchema as JSONSchema, required.has(key))
|
|
261
|
+
const checks = generatePropertyChecks(key, propSchema as JSONSchema, required.has(key), suffix)
|
|
262
262
|
if (checks.length > 0) {
|
|
263
263
|
propertyLines.push(...checks)
|
|
264
264
|
}
|
|
@@ -270,7 +270,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
270
270
|
isSchemaObject(schema.additionalProperties) &&
|
|
271
271
|
hasRef(schema.additionalProperties)
|
|
272
272
|
) {
|
|
273
|
-
const vRefName = validatorName(refToName(schema.additionalProperties.$ref))
|
|
273
|
+
const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix))
|
|
274
274
|
propertyLines.push(` for (const _key of Object.keys(obj)) {`)
|
|
275
275
|
propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`)
|
|
276
276
|
propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`)
|
|
@@ -297,7 +297,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
297
297
|
/**
|
|
298
298
|
* Generates a validator function for a non-object schema (primitive, array, enum, $ref).
|
|
299
299
|
*/
|
|
300
|
-
const generateScalarValidator = (schema: JSONSchema, typeName: string): string => {
|
|
300
|
+
const generateScalarValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
|
|
301
301
|
const vName = validatorName(typeName)
|
|
302
302
|
|
|
303
303
|
if (!isSchemaObject(schema)) {
|
|
@@ -308,7 +308,7 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
308
308
|
|
|
309
309
|
// Top-level $ref — delegate entirely
|
|
310
310
|
if (hasRef(schema)) {
|
|
311
|
-
const delegateName = validatorName(refToName(schema.$ref))
|
|
311
|
+
const delegateName = validatorName(refToName(schema.$ref, suffix))
|
|
312
312
|
return [
|
|
313
313
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
314
314
|
` return ${delegateName}(input, _path)`,
|
|
@@ -361,7 +361,7 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
361
361
|
const branches = schema.oneOf
|
|
362
362
|
.map((branch, i) => {
|
|
363
363
|
if (!hasRef(branch)) return null
|
|
364
|
-
const bName = validatorName(refToName((branch as { $ref: string }).$ref))
|
|
364
|
+
const bName = validatorName(refToName((branch as { $ref: string }).$ref, suffix))
|
|
365
365
|
return ` const _r${i} = ${bName}(input, _path)\n if (_r${i} === true) return true`
|
|
366
366
|
})
|
|
367
367
|
.filter(Boolean)
|
|
@@ -463,10 +463,10 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
463
463
|
* // }
|
|
464
464
|
* ```
|
|
465
465
|
*/
|
|
466
|
-
export const generateValidatorFunction = (schema: JSONSchema, typeName: string): string => {
|
|
466
|
+
export const generateValidatorFunction = (schema: JSONSchema, typeName: string, suffix = ''): string => {
|
|
467
467
|
if (isObjectSchema(schema)) {
|
|
468
|
-
return generateObjectValidator(schema, typeName)
|
|
468
|
+
return generateObjectValidator(schema, typeName, suffix)
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
return generateScalarValidator(schema, typeName)
|
|
471
|
+
return generateScalarValidator(schema, typeName, suffix)
|
|
472
472
|
}
|