@alt-stack/zod-openapi 1.1.3 → 1.3.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/.turbo/turbo-build.log +10 -10
- package/dist/index.cjs +229 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -3
- package/dist/index.d.ts +34 -3
- package/dist/index.js +226 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/interface-generator.spec.ts +668 -0
- package/src/interface-generator.ts +290 -0
- package/src/registry.ts +32 -2
- package/src/to-typescript.spec.ts +60 -0
- package/src/to-typescript.ts +39 -3
- package/src/to-zod.spec.ts +10 -6
package/dist/index.js
CHANGED
|
@@ -231,6 +231,9 @@ function isStringRegistration(reg) {
|
|
|
231
231
|
function isStringsRegistration(reg) {
|
|
232
232
|
return reg.type === "string" && "formats" in reg;
|
|
233
233
|
}
|
|
234
|
+
function isSupportedStringFormat(format) {
|
|
235
|
+
return Object.prototype.hasOwnProperty.call(SUPPORTED_STRING_FORMATS_MAP, format);
|
|
236
|
+
}
|
|
234
237
|
function getTypeFormatPairs(reg) {
|
|
235
238
|
if (isStringRegistration(reg)) {
|
|
236
239
|
return [{ type: "string", format: reg.format }];
|
|
@@ -281,6 +284,7 @@ var ZodSchemaRegistry = class {
|
|
|
281
284
|
* Reverse-lookup helper: given a string format, return the registered schema's exported variable name
|
|
282
285
|
*/
|
|
283
286
|
getSchemaExportedVariableNameForStringFormat(format) {
|
|
287
|
+
if (!isSupportedStringFormat(format)) return void 0;
|
|
284
288
|
for (const registration of this.map.values()) {
|
|
285
289
|
if (registration.type !== "string") continue;
|
|
286
290
|
if (isStringRegistration(registration) && registration.format === format) {
|
|
@@ -292,6 +296,17 @@ var ZodSchemaRegistry = class {
|
|
|
292
296
|
}
|
|
293
297
|
return void 0;
|
|
294
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Reverse-lookup helper: given a primitive type, return the registered schema's exported variable name
|
|
301
|
+
*/
|
|
302
|
+
getSchemaExportedVariableNameForPrimitiveType(type) {
|
|
303
|
+
for (const registration of this.map.values()) {
|
|
304
|
+
if (registration.type === type) {
|
|
305
|
+
return registration.schemaExportedVariableName;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
295
310
|
};
|
|
296
311
|
var schemaRegistry = new ZodSchemaRegistry();
|
|
297
312
|
function registerZodSchemaToOpenApiSchema(schema, openApiSchema) {
|
|
@@ -300,6 +315,9 @@ function registerZodSchemaToOpenApiSchema(schema, openApiSchema) {
|
|
|
300
315
|
function getSchemaExportedVariableNameForStringFormat(format) {
|
|
301
316
|
return schemaRegistry.getSchemaExportedVariableNameForStringFormat(format);
|
|
302
317
|
}
|
|
318
|
+
function getSchemaExportedVariableNameForPrimitiveType(type) {
|
|
319
|
+
return schemaRegistry.getSchemaExportedVariableNameForPrimitiveType(type);
|
|
320
|
+
}
|
|
303
321
|
function clearZodSchemaToOpenApiSchemaRegistry() {
|
|
304
322
|
schemaRegistry.clear();
|
|
305
323
|
}
|
|
@@ -669,11 +687,188 @@ function generateRouteSchemaNames(route) {
|
|
|
669
687
|
return result;
|
|
670
688
|
}
|
|
671
689
|
|
|
672
|
-
// src/
|
|
690
|
+
// src/interface-generator.ts
|
|
673
691
|
var validIdentifierRegex2 = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
674
692
|
function quotePropertyName2(name) {
|
|
675
693
|
return validIdentifierRegex2.test(name) ? name : `'${name}'`;
|
|
676
694
|
}
|
|
695
|
+
function toPascalCase2(name) {
|
|
696
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9]/g, " ").split(" ").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
697
|
+
}
|
|
698
|
+
function schemaExportNameToOutputAlias(name) {
|
|
699
|
+
return `${toPascalCase2(name)}Output`;
|
|
700
|
+
}
|
|
701
|
+
function registerOutputSchemaName(schemaName, options) {
|
|
702
|
+
options?.outputSchemaNames?.add(schemaName);
|
|
703
|
+
return schemaExportNameToOutputAlias(schemaName);
|
|
704
|
+
}
|
|
705
|
+
function getRegisteredOutputAlias(schema, options) {
|
|
706
|
+
if (!schema || typeof schema !== "object") return void 0;
|
|
707
|
+
if (schema["type"] === "string" && typeof schema["format"] === "string") {
|
|
708
|
+
const customSchemaName = getSchemaExportedVariableNameForStringFormat(
|
|
709
|
+
schema["format"]
|
|
710
|
+
);
|
|
711
|
+
if (customSchemaName) {
|
|
712
|
+
return registerOutputSchemaName(customSchemaName, options);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (schema["type"] === "number" || schema["type"] === "integer" || schema["type"] === "boolean") {
|
|
716
|
+
const customSchemaName = getSchemaExportedVariableNameForPrimitiveType(
|
|
717
|
+
schema["type"]
|
|
718
|
+
);
|
|
719
|
+
if (customSchemaName) {
|
|
720
|
+
return registerOutputSchemaName(customSchemaName, options);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
function schemaToTypeString(schema, options) {
|
|
726
|
+
if (!schema || typeof schema !== "object") return "unknown";
|
|
727
|
+
if (schema["$ref"] && typeof schema["$ref"] === "string") {
|
|
728
|
+
const match = schema["$ref"].match(
|
|
729
|
+
/#\/components\/schemas\/(.+)/
|
|
730
|
+
);
|
|
731
|
+
let result2 = "unknown";
|
|
732
|
+
if (match && match[1]) {
|
|
733
|
+
result2 = decodeURIComponent(match[1]);
|
|
734
|
+
}
|
|
735
|
+
if (schema["nullable"] === true) {
|
|
736
|
+
result2 = `(${result2} | null)`;
|
|
737
|
+
}
|
|
738
|
+
return result2;
|
|
739
|
+
}
|
|
740
|
+
let result = "unknown";
|
|
741
|
+
if ("oneOf" in schema && Array.isArray(schema["oneOf"])) {
|
|
742
|
+
const unionMembers = schema["oneOf"].map(
|
|
743
|
+
(s) => schemaToTypeString(s, options)
|
|
744
|
+
);
|
|
745
|
+
result = unionMembers.length > 1 ? `(${unionMembers.join(" | ")})` : unionMembers[0] ?? "unknown";
|
|
746
|
+
} else if ("allOf" in schema && Array.isArray(schema["allOf"])) {
|
|
747
|
+
const intersectionMembers = schema["allOf"].map(
|
|
748
|
+
(s) => schemaToTypeString(s, options)
|
|
749
|
+
);
|
|
750
|
+
result = intersectionMembers.length > 1 ? `(${intersectionMembers.join(" & ")})` : intersectionMembers[0] ?? "unknown";
|
|
751
|
+
} else if ("anyOf" in schema && Array.isArray(schema["anyOf"])) {
|
|
752
|
+
const unionMembers = schema["anyOf"].map(
|
|
753
|
+
(s) => schemaToTypeString(s, options)
|
|
754
|
+
);
|
|
755
|
+
result = unionMembers.length > 1 ? `(${unionMembers.join(" | ")})` : unionMembers[0] ?? "unknown";
|
|
756
|
+
} else {
|
|
757
|
+
switch (schema["type"]) {
|
|
758
|
+
case "string": {
|
|
759
|
+
const registeredAlias = getRegisteredOutputAlias(schema, options);
|
|
760
|
+
if (registeredAlias) {
|
|
761
|
+
result = registeredAlias;
|
|
762
|
+
} else if (schema["enum"] && Array.isArray(schema["enum"])) {
|
|
763
|
+
result = schema["enum"].map((v) => JSON.stringify(v)).join(" | ");
|
|
764
|
+
} else {
|
|
765
|
+
result = "string";
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "number":
|
|
770
|
+
case "integer": {
|
|
771
|
+
const registeredAlias = getRegisteredOutputAlias(schema, options);
|
|
772
|
+
if (registeredAlias) {
|
|
773
|
+
result = registeredAlias;
|
|
774
|
+
} else if (schema["enum"] && Array.isArray(schema["enum"])) {
|
|
775
|
+
result = schema["enum"].map((v) => String(v)).join(" | ");
|
|
776
|
+
} else {
|
|
777
|
+
result = "number";
|
|
778
|
+
}
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
case "boolean":
|
|
782
|
+
result = getRegisteredOutputAlias(schema, options) ?? "boolean";
|
|
783
|
+
break;
|
|
784
|
+
case "null":
|
|
785
|
+
result = "null";
|
|
786
|
+
break;
|
|
787
|
+
case "array":
|
|
788
|
+
if (schema["items"]) {
|
|
789
|
+
const itemType = schemaToTypeString(schema["items"], options);
|
|
790
|
+
result = `Array<${itemType}>`;
|
|
791
|
+
} else {
|
|
792
|
+
result = "unknown[]";
|
|
793
|
+
}
|
|
794
|
+
break;
|
|
795
|
+
case "object":
|
|
796
|
+
result = objectSchemaToTypeString(schema, options);
|
|
797
|
+
break;
|
|
798
|
+
default:
|
|
799
|
+
if (schema["properties"]) {
|
|
800
|
+
result = objectSchemaToTypeString(schema, options);
|
|
801
|
+
} else if (schema["enum"] && Array.isArray(schema["enum"])) {
|
|
802
|
+
result = schema["enum"].map((v) => JSON.stringify(v)).join(" | ");
|
|
803
|
+
} else {
|
|
804
|
+
result = "unknown";
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (schema["nullable"] === true) {
|
|
810
|
+
result = `(${result} | null)`;
|
|
811
|
+
}
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
814
|
+
function objectSchemaToTypeString(schema, options) {
|
|
815
|
+
const properties = schema["properties"];
|
|
816
|
+
const required = new Set(schema["required"] ?? []);
|
|
817
|
+
const additionalProperties = schema["additionalProperties"];
|
|
818
|
+
if (!properties && !additionalProperties) {
|
|
819
|
+
return "Record<string, unknown>";
|
|
820
|
+
}
|
|
821
|
+
const propertyStrings = [];
|
|
822
|
+
if (properties) {
|
|
823
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
824
|
+
const isRequired = required.has(propName);
|
|
825
|
+
const propType = schemaToTypeString(propSchema, options);
|
|
826
|
+
const quotedName = quotePropertyName2(propName);
|
|
827
|
+
propertyStrings.push(
|
|
828
|
+
`${quotedName}${isRequired ? "" : "?"}: ${propType}`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (additionalProperties === true) {
|
|
833
|
+
propertyStrings.push("[key: string]: unknown");
|
|
834
|
+
} else if (typeof additionalProperties === "object" && additionalProperties !== null) {
|
|
835
|
+
const additionalType = schemaToTypeString(additionalProperties, options);
|
|
836
|
+
propertyStrings.push(`[key: string]: ${additionalType}`);
|
|
837
|
+
}
|
|
838
|
+
return `{ ${propertyStrings.join("; ")} }`;
|
|
839
|
+
}
|
|
840
|
+
function generateInterface(name, schema, options) {
|
|
841
|
+
const properties = schema["properties"];
|
|
842
|
+
const required = new Set(schema["required"] ?? []);
|
|
843
|
+
if (schema["type"] !== "object" && !properties) {
|
|
844
|
+
return `export type ${name} = ${schemaToTypeString(schema, options)};`;
|
|
845
|
+
}
|
|
846
|
+
const lines = [];
|
|
847
|
+
lines.push(`export interface ${name} {`);
|
|
848
|
+
if (properties) {
|
|
849
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
850
|
+
const isRequired = required.has(propName);
|
|
851
|
+
const propType = schemaToTypeString(propSchema, options);
|
|
852
|
+
const quotedName = quotePropertyName2(propName);
|
|
853
|
+
lines.push(` ${quotedName}${isRequired ? "" : "?"}: ${propType};`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const additionalProperties = schema["additionalProperties"];
|
|
857
|
+
if (additionalProperties === true) {
|
|
858
|
+
lines.push(" [key: string]: unknown;");
|
|
859
|
+
} else if (typeof additionalProperties === "object" && additionalProperties !== null) {
|
|
860
|
+
const additionalType = schemaToTypeString(additionalProperties, options);
|
|
861
|
+
lines.push(` [key: string]: ${additionalType};`);
|
|
862
|
+
}
|
|
863
|
+
lines.push("}");
|
|
864
|
+
return lines.join("\n");
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/to-typescript.ts
|
|
868
|
+
var validIdentifierRegex3 = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
869
|
+
function quotePropertyName3(name) {
|
|
870
|
+
return validIdentifierRegex3.test(name) ? name : `'${name}'`;
|
|
871
|
+
}
|
|
677
872
|
function generateRouteSchemaName2(path, method, suffix) {
|
|
678
873
|
const pathParts = path.split("/").filter((p) => p).map((p) => {
|
|
679
874
|
if (p.startsWith("{") && p.endsWith("}")) {
|
|
@@ -715,7 +910,7 @@ function generateRouteSchemas(routes, convertSchema, registry) {
|
|
|
715
910
|
const properties = [];
|
|
716
911
|
for (const param of pathParams) {
|
|
717
912
|
const zodExpr = convertSchema(param.schema);
|
|
718
|
-
properties.push(`${
|
|
913
|
+
properties.push(`${quotePropertyName3(param.name)}: ${zodExpr}`);
|
|
719
914
|
}
|
|
720
915
|
declarations.push(
|
|
721
916
|
`export const ${names.paramsSchemaName} = z.object({ ${properties.join(", ")} });`
|
|
@@ -751,7 +946,7 @@ function generateRouteSchemas(routes, convertSchema, registry) {
|
|
|
751
946
|
if (!param.required) {
|
|
752
947
|
zodExpr += ".optional()";
|
|
753
948
|
}
|
|
754
|
-
properties.push(`${
|
|
949
|
+
properties.push(`${quotePropertyName3(param.name)}: ${zodExpr}`);
|
|
755
950
|
}
|
|
756
951
|
declarations.push(
|
|
757
952
|
`export const ${names.querySchemaName} = z.object({ ${properties.join(", ")} });`
|
|
@@ -787,7 +982,7 @@ function generateRouteSchemas(routes, convertSchema, registry) {
|
|
|
787
982
|
if (!param.required) {
|
|
788
983
|
zodExpr += ".optional()";
|
|
789
984
|
}
|
|
790
|
-
properties.push(`${
|
|
985
|
+
properties.push(`${quotePropertyName3(param.name)}: ${zodExpr}`);
|
|
791
986
|
}
|
|
792
987
|
declarations.push(
|
|
793
988
|
`export const ${names.headersSchemaName} = z.object({ ${properties.join(", ")} });`
|
|
@@ -982,21 +1177,42 @@ var openApiToZodTsCode = (openapi, customImportLines, options) => {
|
|
|
982
1177
|
lines.push("import { z } from 'zod';");
|
|
983
1178
|
lines.push(...customImportLines ?? []);
|
|
984
1179
|
lines.push("");
|
|
1180
|
+
lines.push("// Type assertion helper - verifies interface matches schema at compile time");
|
|
1181
|
+
lines.push("type _AssertEqual<T, U> = [T] extends [U] ? ([U] extends [T] ? true : never) : never;");
|
|
1182
|
+
lines.push("");
|
|
985
1183
|
const registry = createSchemaRegistry();
|
|
986
1184
|
const sortedSchemaNames = topologicalSortSchemas(schemas);
|
|
1185
|
+
const typeAssertions = [];
|
|
1186
|
+
const outputSchemaNames = /* @__PURE__ */ new Set();
|
|
1187
|
+
const schemaBlocks = [];
|
|
987
1188
|
for (const name of sortedSchemaNames) {
|
|
988
1189
|
const schema = schemas[name];
|
|
989
1190
|
if (schema) {
|
|
990
1191
|
const zodExpr = convertSchemaToZodString(schema);
|
|
991
1192
|
const schemaName = `${name}Schema`;
|
|
992
1193
|
const typeName = name;
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1194
|
+
schemaBlocks.push(generateInterface(typeName, schema, { outputSchemaNames }));
|
|
1195
|
+
schemaBlocks.push(`export const ${schemaName}: z.ZodType<${typeName}> = ${zodExpr};`);
|
|
1196
|
+
schemaBlocks.push("");
|
|
1197
|
+
typeAssertions.push(`type _Assert${typeName} = _AssertEqual<${typeName}, z.infer<typeof ${schemaName}>>;`);
|
|
996
1198
|
const fingerprint = getSchemaFingerprint(schema);
|
|
997
1199
|
preRegisterSchema(registry, schemaName, fingerprint);
|
|
998
1200
|
}
|
|
999
1201
|
}
|
|
1202
|
+
if (outputSchemaNames.size > 0) {
|
|
1203
|
+
lines.push("// Zod output aliases for registered schemas");
|
|
1204
|
+
for (const schemaName of outputSchemaNames) {
|
|
1205
|
+
const aliasName = schemaExportNameToOutputAlias(schemaName);
|
|
1206
|
+
lines.push(`type ${aliasName} = z.output<typeof ${schemaName}>;`);
|
|
1207
|
+
}
|
|
1208
|
+
lines.push("");
|
|
1209
|
+
}
|
|
1210
|
+
lines.push(...schemaBlocks);
|
|
1211
|
+
if (typeAssertions.length > 0) {
|
|
1212
|
+
lines.push("// Compile-time type assertions - ensure interfaces match schemas");
|
|
1213
|
+
lines.push(typeAssertions.join("\n"));
|
|
1214
|
+
lines.push("");
|
|
1215
|
+
}
|
|
1000
1216
|
if (options?.includeRoutes) {
|
|
1001
1217
|
const routes = parseOpenApiPaths(openapi);
|
|
1002
1218
|
if (routes.length > 0) {
|
|
@@ -1034,11 +1250,13 @@ export {
|
|
|
1034
1250
|
SUPPORTED_STRING_FORMATS,
|
|
1035
1251
|
clearZodSchemaToOpenApiSchemaRegistry,
|
|
1036
1252
|
convertSchemaToZodString,
|
|
1253
|
+
generateInterface,
|
|
1037
1254
|
generateRouteSchemaNames,
|
|
1038
1255
|
getSchemaExportedVariableNameForStringFormat,
|
|
1039
1256
|
openApiToZodTsCode,
|
|
1040
1257
|
parseOpenApiPaths,
|
|
1041
1258
|
registerZodSchemaToOpenApiSchema,
|
|
1042
|
-
schemaRegistry
|
|
1259
|
+
schemaRegistry,
|
|
1260
|
+
schemaToTypeString
|
|
1043
1261
|
};
|
|
1044
1262
|
//# sourceMappingURL=index.js.map
|