@amritk/generate-validators 0.3.1 → 0.4.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.
- 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 +40 -35
- 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
|
}
|
|
@@ -787,12 +787,12 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
|
|
|
787
787
|
if (isSchemaObject2(schema) && Array.isArray(schema.allOf)) {
|
|
788
788
|
for (const entry of schema.allOf) {
|
|
789
789
|
if (isSchemaObject2(entry) && entry.$ref) {
|
|
790
|
-
allOfIntersections.push(refToName2(entry.$ref));
|
|
790
|
+
allOfIntersections.push(refToName2(entry.$ref, options.typeSuffix));
|
|
791
791
|
}
|
|
792
792
|
}
|
|
793
793
|
}
|
|
794
794
|
if (isSchemaObject2(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#")) {
|
|
795
|
-
allOfIntersections.push(refToName2(schema.$ref));
|
|
795
|
+
allOfIntersections.push(refToName2(schema.$ref, options.typeSuffix));
|
|
796
796
|
}
|
|
797
797
|
let result = "";
|
|
798
798
|
if (jsDocTitle && jsDocDescription) {
|
|
@@ -802,7 +802,7 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
|
|
|
802
802
|
` + properties + `
|
|
803
803
|
}`;
|
|
804
804
|
if (conditionalThenRef) {
|
|
805
|
-
typeBody += " & " + refToName2(conditionalThenRef);
|
|
805
|
+
typeBody += " & " + refToName2(conditionalThenRef, options.typeSuffix);
|
|
806
806
|
}
|
|
807
807
|
for (const intersectionType of allOfIntersections) {
|
|
808
808
|
typeBody += " & " + intersectionType;
|
|
@@ -876,9 +876,9 @@ var hasMultipleOf = (schema) => {
|
|
|
876
876
|
};
|
|
877
877
|
|
|
878
878
|
// src/generators/collect-validator-imports.ts
|
|
879
|
-
var buildImport = (ref) => {
|
|
879
|
+
var buildImport = (ref, suffix) => {
|
|
880
880
|
const filename = refToFilename(ref);
|
|
881
|
-
const typeName = refToName(ref);
|
|
881
|
+
const typeName = refToName(ref, suffix);
|
|
882
882
|
const validatorName = `validate${typeName}`;
|
|
883
883
|
return `import { type ${typeName}, ${validatorName} } from './${filename}'`;
|
|
884
884
|
};
|
|
@@ -923,6 +923,7 @@ var collectDirectRefs = (schema) => {
|
|
|
923
923
|
var collectValidatorImports = (schema, options) => {
|
|
924
924
|
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
|
|
925
925
|
const rootSchema = options?.rootSchema;
|
|
926
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
926
927
|
const refs = collectDirectRefs(schema);
|
|
927
928
|
const seen = new Set;
|
|
928
929
|
const imports = [];
|
|
@@ -939,7 +940,7 @@ var collectValidatorImports = (schema, options) => {
|
|
|
939
940
|
}
|
|
940
941
|
seen.add(filename);
|
|
941
942
|
const importRef = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
|
|
942
|
-
imports.push(buildImport(importRef));
|
|
943
|
+
imports.push(buildImport(importRef, typeSuffix));
|
|
943
944
|
}
|
|
944
945
|
return imports;
|
|
945
946
|
};
|
|
@@ -993,7 +994,7 @@ var wrongTypeCondition = (accessor, type) => {
|
|
|
993
994
|
return "";
|
|
994
995
|
}
|
|
995
996
|
};
|
|
996
|
-
var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
997
|
+
var generatePropertyChecks = (key, propSchema, isRequired, suffix) => {
|
|
997
998
|
if (!isSchemaObject3(propSchema))
|
|
998
999
|
return [];
|
|
999
1000
|
const raw = `obj[${JSON.stringify(key)}]`;
|
|
@@ -1001,7 +1002,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
|
1001
1002
|
const lines = [];
|
|
1002
1003
|
if (hasRef(propSchema)) {
|
|
1003
1004
|
const ref = propSchema.$ref;
|
|
1004
|
-
const vName = validatorName(refToName(ref));
|
|
1005
|
+
const vName = validatorName(refToName(ref, suffix));
|
|
1005
1006
|
if (isRequired) {
|
|
1006
1007
|
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
|
|
1007
1008
|
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
|
|
@@ -1127,7 +1128,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
|
1127
1128
|
if (t === "array" && hasItems(propSchema)) {
|
|
1128
1129
|
const itemSchema = propSchema.items;
|
|
1129
1130
|
if (hasRef(itemSchema)) {
|
|
1130
|
-
const vName = validatorName(refToName(itemSchema.$ref));
|
|
1131
|
+
const vName = validatorName(refToName(itemSchema.$ref, suffix));
|
|
1131
1132
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
1132
1133
|
lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
|
|
1133
1134
|
lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`);
|
|
@@ -1151,19 +1152,19 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
|
1151
1152
|
}
|
|
1152
1153
|
return lines;
|
|
1153
1154
|
};
|
|
1154
|
-
var generateObjectValidator = (schema, typeName) => {
|
|
1155
|
+
var generateObjectValidator = (schema, typeName, suffix) => {
|
|
1155
1156
|
const vName = validatorName(typeName);
|
|
1156
1157
|
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
1157
1158
|
const properties = hasProperties(schema) ? schema.properties : {};
|
|
1158
1159
|
const propertyLines = [];
|
|
1159
1160
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
1160
|
-
const checks = generatePropertyChecks(key, propSchema, required.has(key));
|
|
1161
|
+
const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix);
|
|
1161
1162
|
if (checks.length > 0) {
|
|
1162
1163
|
propertyLines.push(...checks);
|
|
1163
1164
|
}
|
|
1164
1165
|
}
|
|
1165
1166
|
if (hasAdditionalProperties(schema) && isSchemaObject3(schema.additionalProperties) && hasRef(schema.additionalProperties)) {
|
|
1166
|
-
const vRefName = validatorName(refToName(schema.additionalProperties.$ref));
|
|
1167
|
+
const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix));
|
|
1167
1168
|
propertyLines.push(` for (const _key of Object.keys(obj)) {`);
|
|
1168
1169
|
propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`);
|
|
1169
1170
|
propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`);
|
|
@@ -1188,14 +1189,14 @@ var generateObjectValidator = (schema, typeName) => {
|
|
|
1188
1189
|
].join(`
|
|
1189
1190
|
`);
|
|
1190
1191
|
};
|
|
1191
|
-
var generateScalarValidator = (schema, typeName) => {
|
|
1192
|
+
var generateScalarValidator = (schema, typeName, suffix) => {
|
|
1192
1193
|
const vName = validatorName(typeName);
|
|
1193
1194
|
if (!isSchemaObject3(schema)) {
|
|
1194
1195
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
|
|
1195
1196
|
`);
|
|
1196
1197
|
}
|
|
1197
1198
|
if (hasRef(schema)) {
|
|
1198
|
-
const delegateName = validatorName(refToName(schema.$ref));
|
|
1199
|
+
const delegateName = validatorName(refToName(schema.$ref, suffix));
|
|
1199
1200
|
return [
|
|
1200
1201
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1201
1202
|
` return ${delegateName}(input, _path)`,
|
|
@@ -1244,7 +1245,7 @@ var generateScalarValidator = (schema, typeName) => {
|
|
|
1244
1245
|
const branches = schema.oneOf.map((branch, i) => {
|
|
1245
1246
|
if (!hasRef(branch))
|
|
1246
1247
|
return null;
|
|
1247
|
-
const bName = validatorName(refToName(branch.$ref));
|
|
1248
|
+
const bName = validatorName(refToName(branch.$ref, suffix));
|
|
1248
1249
|
return ` const _r${i} = ${bName}(input, _path)
|
|
1249
1250
|
if (_r${i} === true) return true`;
|
|
1250
1251
|
}).filter(Boolean).join(`
|
|
@@ -1314,21 +1315,23 @@ var generateScalarValidator = (schema, typeName) => {
|
|
|
1314
1315
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
|
|
1315
1316
|
`);
|
|
1316
1317
|
};
|
|
1317
|
-
var generateValidatorFunction = (schema, typeName) => {
|
|
1318
|
+
var generateValidatorFunction = (schema, typeName, suffix = "") => {
|
|
1318
1319
|
if (isObjectSchema2(schema)) {
|
|
1319
|
-
return generateObjectValidator(schema, typeName);
|
|
1320
|
+
return generateObjectValidator(schema, typeName, suffix);
|
|
1320
1321
|
}
|
|
1321
|
-
return generateScalarValidator(schema, typeName);
|
|
1322
|
+
return generateScalarValidator(schema, typeName, suffix);
|
|
1322
1323
|
};
|
|
1323
1324
|
|
|
1324
1325
|
// src/generators/generate-files.ts
|
|
1325
1326
|
var generateValidatorFile = (schema, typeName, options) => {
|
|
1327
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
1326
1328
|
const refImports = collectValidatorImports(schema, {
|
|
1327
1329
|
selfRef: options?.selfRef,
|
|
1328
|
-
rootSchema: options?.rootSchema
|
|
1330
|
+
rootSchema: options?.rootSchema,
|
|
1331
|
+
typeSuffix
|
|
1329
1332
|
});
|
|
1330
|
-
const typeDefinition = generateTypeDefinition(schema, typeName);
|
|
1331
|
-
const validatorFunction = generateValidatorFunction(schema, typeName);
|
|
1333
|
+
const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
|
|
1334
|
+
const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
|
|
1332
1335
|
let result = `import type { ValidationResult, ValidationError } from './validation-result'
|
|
1333
1336
|
`;
|
|
1334
1337
|
for (const imp of refImports) {
|
|
@@ -1365,7 +1368,7 @@ export type ValidationError = {
|
|
|
1365
1368
|
*/
|
|
1366
1369
|
export type ValidationResult = true | { valid: false; errors: ValidationError[] }
|
|
1367
1370
|
`;
|
|
1368
|
-
var buildValidatorSchema = async (rootSchema, rootTypeName) => {
|
|
1371
|
+
var buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "") => {
|
|
1369
1372
|
rootSchema = upgradeDraft07Schema(rootSchema);
|
|
1370
1373
|
const files = [];
|
|
1371
1374
|
const processedRefs = new Set;
|
|
@@ -1374,7 +1377,8 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
|
|
|
1374
1377
|
const dynamicRefMap = buildDynamicRefMap(rootSchema);
|
|
1375
1378
|
const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap);
|
|
1376
1379
|
const rootContent = generateValidatorFile(processedRootSchema, rootTypeName, {
|
|
1377
|
-
rootSchema
|
|
1380
|
+
rootSchema,
|
|
1381
|
+
typeSuffix
|
|
1378
1382
|
});
|
|
1379
1383
|
const rootFilename = rootTypeName.toLowerCase();
|
|
1380
1384
|
if (rootFilename !== "validation-result") {
|
|
@@ -1393,12 +1397,13 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
|
|
|
1393
1397
|
console.warn(`Warning: Could not resolve ref: ${ref}`);
|
|
1394
1398
|
continue;
|
|
1395
1399
|
}
|
|
1396
|
-
const typeName = refToName(ref);
|
|
1400
|
+
const typeName = refToName(ref, typeSuffix);
|
|
1397
1401
|
const filename = refToFilename(ref);
|
|
1398
1402
|
const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap);
|
|
1399
1403
|
const content = generateValidatorFile(processedSchema, typeName, {
|
|
1400
1404
|
selfRef: ref,
|
|
1401
|
-
rootSchema
|
|
1405
|
+
rootSchema,
|
|
1406
|
+
typeSuffix
|
|
1402
1407
|
});
|
|
1403
1408
|
if (filename !== "validation-result" && !processedFilenames.has(filename)) {
|
|
1404
1409
|
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.0",
|
|
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.0"
|
|
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
|
}
|