@alt-stack/zod-openapi 1.1.2 → 1.1.3
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 +8 -8
- package/dist/index.cjs +243 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +243 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/schema-dedup.ts +199 -0
- package/src/to-typescript.spec.ts +218 -0
- package/src/to-typescript.ts +228 -34
package/dist/index.js
CHANGED
|
@@ -108,6 +108,87 @@ function topologicalSortSchemas(schemas) {
|
|
|
108
108
|
return sorted;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// src/schema-dedup.ts
|
|
112
|
+
function sortObjectDeep(obj) {
|
|
113
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
114
|
+
if (Array.isArray(obj)) return obj.map(sortObjectDeep);
|
|
115
|
+
const sorted = {};
|
|
116
|
+
const keys = Object.keys(obj).sort();
|
|
117
|
+
for (const key of keys) {
|
|
118
|
+
sorted[key] = sortObjectDeep(obj[key]);
|
|
119
|
+
}
|
|
120
|
+
return sorted;
|
|
121
|
+
}
|
|
122
|
+
function getSchemaFingerprint(schema) {
|
|
123
|
+
return JSON.stringify(sortObjectDeep(schema));
|
|
124
|
+
}
|
|
125
|
+
function createSchemaRegistry() {
|
|
126
|
+
return {
|
|
127
|
+
fingerprintToName: /* @__PURE__ */ new Map(),
|
|
128
|
+
nameToFingerprint: /* @__PURE__ */ new Map()
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function registerSchema(registry, name, schema) {
|
|
132
|
+
const fingerprint = getSchemaFingerprint(schema);
|
|
133
|
+
const existing = registry.fingerprintToName.get(fingerprint);
|
|
134
|
+
if (existing) {
|
|
135
|
+
return { isNew: false, canonicalName: existing };
|
|
136
|
+
}
|
|
137
|
+
registry.fingerprintToName.set(fingerprint, name);
|
|
138
|
+
registry.nameToFingerprint.set(name, fingerprint);
|
|
139
|
+
return { isNew: true, canonicalName: name };
|
|
140
|
+
}
|
|
141
|
+
function preRegisterSchema(registry, name, fingerprint) {
|
|
142
|
+
registry.fingerprintToName.set(fingerprint, name);
|
|
143
|
+
registry.nameToFingerprint.set(name, fingerprint);
|
|
144
|
+
}
|
|
145
|
+
function extractErrorCode(schema) {
|
|
146
|
+
const properties = schema?.["properties"];
|
|
147
|
+
const errorObj = properties?.["error"];
|
|
148
|
+
const errorProps = errorObj?.["properties"];
|
|
149
|
+
const codeSchema = errorProps?.["code"];
|
|
150
|
+
const codeEnum = codeSchema?.["enum"];
|
|
151
|
+
if (Array.isArray(codeEnum) && codeEnum.length === 1) {
|
|
152
|
+
return codeEnum[0];
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function errorCodeToPascalCase(code) {
|
|
157
|
+
return code.split("_").map((part) => part.charAt(0) + part.slice(1).toLowerCase()).join("");
|
|
158
|
+
}
|
|
159
|
+
function generateCommonErrorSchemaName(errorCode) {
|
|
160
|
+
return `${errorCodeToPascalCase(errorCode)}ErrorSchema`;
|
|
161
|
+
}
|
|
162
|
+
function findCommonSchemas(schemas, minCount = 2) {
|
|
163
|
+
const fingerprints = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const { name, schema } of schemas) {
|
|
165
|
+
const fingerprint = getSchemaFingerprint(schema);
|
|
166
|
+
const existing = fingerprints.get(fingerprint);
|
|
167
|
+
if (existing) {
|
|
168
|
+
existing.names.push(name);
|
|
169
|
+
} else {
|
|
170
|
+
fingerprints.set(fingerprint, {
|
|
171
|
+
schema,
|
|
172
|
+
names: [name],
|
|
173
|
+
errorCode: extractErrorCode(schema)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const commonSchemas = [];
|
|
178
|
+
for (const [fingerprint, data] of fingerprints) {
|
|
179
|
+
if (data.names.length >= minCount) {
|
|
180
|
+
const name = data.errorCode ? generateCommonErrorSchemaName(data.errorCode) : data.names[0];
|
|
181
|
+
commonSchemas.push({
|
|
182
|
+
name,
|
|
183
|
+
schema: data.schema,
|
|
184
|
+
fingerprint,
|
|
185
|
+
count: data.names.length
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return commonSchemas.sort((a, b) => b.count - a.count);
|
|
190
|
+
}
|
|
191
|
+
|
|
111
192
|
// src/types/boolean.ts
|
|
112
193
|
function convertOpenAPIBooleanToZod(_) {
|
|
113
194
|
return "z.boolean()";
|
|
@@ -606,34 +687,64 @@ function generateRouteSchemaName2(path, method, suffix) {
|
|
|
606
687
|
const parts = [methodPrefix, ...pathParts, suffix];
|
|
607
688
|
return parts.join("");
|
|
608
689
|
}
|
|
609
|
-
function generateRouteSchemas(routes, convertSchema) {
|
|
610
|
-
const
|
|
611
|
-
const
|
|
690
|
+
function generateRouteSchemas(routes, convertSchema, registry) {
|
|
691
|
+
const declarations = [];
|
|
692
|
+
const schemaNameToCanonical = /* @__PURE__ */ new Map();
|
|
693
|
+
const generatedNames = /* @__PURE__ */ new Set();
|
|
612
694
|
for (const route of routes) {
|
|
613
695
|
const names = generateRouteSchemaNames(route);
|
|
614
696
|
const pathParams = route.parameters.filter((p) => p.in === "path");
|
|
615
697
|
const queryParams = route.parameters.filter((p) => p.in === "query");
|
|
616
698
|
const headerParams = route.parameters.filter((p) => p.in === "header");
|
|
617
699
|
if (names.paramsSchemaName && pathParams.length > 0) {
|
|
618
|
-
|
|
619
|
-
|
|
700
|
+
const paramsSchema = {
|
|
701
|
+
type: "object",
|
|
702
|
+
properties: Object.fromEntries(
|
|
703
|
+
pathParams.map((p) => [p.name, p.schema])
|
|
704
|
+
),
|
|
705
|
+
required: pathParams.filter((p) => p.required).map((p) => p.name)
|
|
706
|
+
};
|
|
707
|
+
const { isNew, canonicalName } = registerSchema(
|
|
708
|
+
registry,
|
|
709
|
+
names.paramsSchemaName,
|
|
710
|
+
paramsSchema
|
|
711
|
+
);
|
|
712
|
+
schemaNameToCanonical.set(names.paramsSchemaName, canonicalName);
|
|
713
|
+
if (isNew && !generatedNames.has(names.paramsSchemaName)) {
|
|
714
|
+
generatedNames.add(names.paramsSchemaName);
|
|
620
715
|
const properties = [];
|
|
621
|
-
const required = [];
|
|
622
716
|
for (const param of pathParams) {
|
|
623
717
|
const zodExpr = convertSchema(param.schema);
|
|
624
718
|
properties.push(`${quotePropertyName2(param.name)}: ${zodExpr}`);
|
|
625
|
-
if (param.required) {
|
|
626
|
-
required.push(param.name);
|
|
627
|
-
}
|
|
628
719
|
}
|
|
629
|
-
|
|
720
|
+
declarations.push(
|
|
630
721
|
`export const ${names.paramsSchemaName} = z.object({ ${properties.join(", ")} });`
|
|
631
722
|
);
|
|
723
|
+
} else if (!isNew && names.paramsSchemaName !== canonicalName) {
|
|
724
|
+
if (!generatedNames.has(names.paramsSchemaName)) {
|
|
725
|
+
generatedNames.add(names.paramsSchemaName);
|
|
726
|
+
declarations.push(
|
|
727
|
+
`export const ${names.paramsSchemaName} = ${canonicalName};`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
632
730
|
}
|
|
633
731
|
}
|
|
634
732
|
if (names.querySchemaName && queryParams.length > 0) {
|
|
635
|
-
|
|
636
|
-
|
|
733
|
+
const querySchema = {
|
|
734
|
+
type: "object",
|
|
735
|
+
properties: Object.fromEntries(
|
|
736
|
+
queryParams.map((p) => [p.name, p.schema])
|
|
737
|
+
),
|
|
738
|
+
required: queryParams.filter((p) => p.required).map((p) => p.name)
|
|
739
|
+
};
|
|
740
|
+
const { isNew, canonicalName } = registerSchema(
|
|
741
|
+
registry,
|
|
742
|
+
names.querySchemaName,
|
|
743
|
+
querySchema
|
|
744
|
+
);
|
|
745
|
+
schemaNameToCanonical.set(names.querySchemaName, canonicalName);
|
|
746
|
+
if (isNew && !generatedNames.has(names.querySchemaName)) {
|
|
747
|
+
generatedNames.add(names.querySchemaName);
|
|
637
748
|
const properties = [];
|
|
638
749
|
for (const param of queryParams) {
|
|
639
750
|
let zodExpr = convertSchema(param.schema);
|
|
@@ -642,14 +753,34 @@ function generateRouteSchemas(routes, convertSchema) {
|
|
|
642
753
|
}
|
|
643
754
|
properties.push(`${quotePropertyName2(param.name)}: ${zodExpr}`);
|
|
644
755
|
}
|
|
645
|
-
|
|
756
|
+
declarations.push(
|
|
646
757
|
`export const ${names.querySchemaName} = z.object({ ${properties.join(", ")} });`
|
|
647
758
|
);
|
|
759
|
+
} else if (!isNew && names.querySchemaName !== canonicalName) {
|
|
760
|
+
if (!generatedNames.has(names.querySchemaName)) {
|
|
761
|
+
generatedNames.add(names.querySchemaName);
|
|
762
|
+
declarations.push(
|
|
763
|
+
`export const ${names.querySchemaName} = ${canonicalName};`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
648
766
|
}
|
|
649
767
|
}
|
|
650
768
|
if (names.headersSchemaName && headerParams.length > 0) {
|
|
651
|
-
|
|
652
|
-
|
|
769
|
+
const headersSchema = {
|
|
770
|
+
type: "object",
|
|
771
|
+
properties: Object.fromEntries(
|
|
772
|
+
headerParams.map((p) => [p.name, p.schema])
|
|
773
|
+
),
|
|
774
|
+
required: headerParams.filter((p) => p.required).map((p) => p.name)
|
|
775
|
+
};
|
|
776
|
+
const { isNew, canonicalName } = registerSchema(
|
|
777
|
+
registry,
|
|
778
|
+
names.headersSchemaName,
|
|
779
|
+
headersSchema
|
|
780
|
+
);
|
|
781
|
+
schemaNameToCanonical.set(names.headersSchemaName, canonicalName);
|
|
782
|
+
if (isNew && !generatedNames.has(names.headersSchemaName)) {
|
|
783
|
+
generatedNames.add(names.headersSchemaName);
|
|
653
784
|
const properties = [];
|
|
654
785
|
for (const param of headerParams) {
|
|
655
786
|
let zodExpr = convertSchema(param.schema);
|
|
@@ -658,16 +789,36 @@ function generateRouteSchemas(routes, convertSchema) {
|
|
|
658
789
|
}
|
|
659
790
|
properties.push(`${quotePropertyName2(param.name)}: ${zodExpr}`);
|
|
660
791
|
}
|
|
661
|
-
|
|
792
|
+
declarations.push(
|
|
662
793
|
`export const ${names.headersSchemaName} = z.object({ ${properties.join(", ")} });`
|
|
663
794
|
);
|
|
795
|
+
} else if (!isNew && names.headersSchemaName !== canonicalName) {
|
|
796
|
+
if (!generatedNames.has(names.headersSchemaName)) {
|
|
797
|
+
generatedNames.add(names.headersSchemaName);
|
|
798
|
+
declarations.push(
|
|
799
|
+
`export const ${names.headersSchemaName} = ${canonicalName};`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
664
802
|
}
|
|
665
803
|
}
|
|
666
804
|
if (names.bodySchemaName && route.requestBody) {
|
|
667
|
-
|
|
668
|
-
|
|
805
|
+
const { isNew, canonicalName } = registerSchema(
|
|
806
|
+
registry,
|
|
807
|
+
names.bodySchemaName,
|
|
808
|
+
route.requestBody
|
|
809
|
+
);
|
|
810
|
+
schemaNameToCanonical.set(names.bodySchemaName, canonicalName);
|
|
811
|
+
if (isNew && !generatedNames.has(names.bodySchemaName)) {
|
|
812
|
+
generatedNames.add(names.bodySchemaName);
|
|
669
813
|
const zodExpr = convertSchema(route.requestBody);
|
|
670
|
-
|
|
814
|
+
declarations.push(`export const ${names.bodySchemaName} = ${zodExpr};`);
|
|
815
|
+
} else if (!isNew && names.bodySchemaName !== canonicalName) {
|
|
816
|
+
if (!generatedNames.has(names.bodySchemaName)) {
|
|
817
|
+
generatedNames.add(names.bodySchemaName);
|
|
818
|
+
declarations.push(
|
|
819
|
+
`export const ${names.bodySchemaName} = ${canonicalName};`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
671
822
|
}
|
|
672
823
|
}
|
|
673
824
|
for (const [statusCode, responseSchema] of Object.entries(
|
|
@@ -681,19 +832,35 @@ function generateRouteSchemas(routes, convertSchema) {
|
|
|
681
832
|
route.method,
|
|
682
833
|
suffix
|
|
683
834
|
);
|
|
684
|
-
|
|
685
|
-
|
|
835
|
+
const { isNew, canonicalName } = registerSchema(
|
|
836
|
+
registry,
|
|
837
|
+
responseSchemaName,
|
|
838
|
+
responseSchema
|
|
839
|
+
);
|
|
840
|
+
schemaNameToCanonical.set(responseSchemaName, canonicalName);
|
|
841
|
+
if (isNew && !generatedNames.has(responseSchemaName)) {
|
|
842
|
+
generatedNames.add(responseSchemaName);
|
|
686
843
|
const zodExpr = convertSchema(responseSchema);
|
|
687
|
-
|
|
844
|
+
declarations.push(`export const ${responseSchemaName} = ${zodExpr};`);
|
|
845
|
+
} else if (!isNew && responseSchemaName !== canonicalName) {
|
|
846
|
+
if (!generatedNames.has(responseSchemaName)) {
|
|
847
|
+
generatedNames.add(responseSchemaName);
|
|
848
|
+
declarations.push(
|
|
849
|
+
`export const ${responseSchemaName} = ${canonicalName};`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
688
852
|
}
|
|
689
853
|
}
|
|
690
854
|
}
|
|
691
|
-
return
|
|
855
|
+
return { declarations, schemaNameToCanonical };
|
|
692
856
|
}
|
|
693
|
-
function generateRequestResponseObjects(routes) {
|
|
857
|
+
function generateRequestResponseObjects(routes, schemaNameToCanonical) {
|
|
694
858
|
const lines = [];
|
|
695
859
|
const requestPaths = {};
|
|
696
860
|
const responsePaths = {};
|
|
861
|
+
const resolveSchemaName = (name) => {
|
|
862
|
+
return schemaNameToCanonical.get(name) ?? name;
|
|
863
|
+
};
|
|
697
864
|
for (const route of routes) {
|
|
698
865
|
const names = generateRouteSchemaNames(route);
|
|
699
866
|
const pathParams = route.parameters.filter((p) => p.in === "path");
|
|
@@ -708,16 +875,20 @@ function generateRequestResponseObjects(routes) {
|
|
|
708
875
|
}
|
|
709
876
|
const requestParts = [];
|
|
710
877
|
if (names.paramsSchemaName && pathParams.length > 0) {
|
|
711
|
-
requestParts.push(
|
|
878
|
+
requestParts.push(
|
|
879
|
+
`params: ${resolveSchemaName(names.paramsSchemaName)}`
|
|
880
|
+
);
|
|
712
881
|
}
|
|
713
882
|
if (names.querySchemaName && queryParams.length > 0) {
|
|
714
|
-
requestParts.push(`query: ${names.querySchemaName}`);
|
|
883
|
+
requestParts.push(`query: ${resolveSchemaName(names.querySchemaName)}`);
|
|
715
884
|
}
|
|
716
885
|
if (names.headersSchemaName && headerParams.length > 0) {
|
|
717
|
-
requestParts.push(
|
|
886
|
+
requestParts.push(
|
|
887
|
+
`headers: ${resolveSchemaName(names.headersSchemaName)}`
|
|
888
|
+
);
|
|
718
889
|
}
|
|
719
890
|
if (names.bodySchemaName && route.requestBody) {
|
|
720
|
-
requestParts.push(`body: ${names.bodySchemaName}`);
|
|
891
|
+
requestParts.push(`body: ${resolveSchemaName(names.bodySchemaName)}`);
|
|
721
892
|
}
|
|
722
893
|
if (requestParts.length > 0) {
|
|
723
894
|
requestMethodObj[route.method] = requestParts;
|
|
@@ -740,7 +911,7 @@ function generateRequestResponseObjects(routes) {
|
|
|
740
911
|
route.method,
|
|
741
912
|
suffix
|
|
742
913
|
);
|
|
743
|
-
responseMethodObj[route.method][statusCode] = responseSchemaName;
|
|
914
|
+
responseMethodObj[route.method][statusCode] = resolveSchemaName(responseSchemaName);
|
|
744
915
|
}
|
|
745
916
|
}
|
|
746
917
|
lines.push("export const Request = {");
|
|
@@ -780,6 +951,25 @@ function generateRequestResponseObjects(routes) {
|
|
|
780
951
|
lines.push("} as const;");
|
|
781
952
|
return lines;
|
|
782
953
|
}
|
|
954
|
+
function collectRouteSchemas(routes) {
|
|
955
|
+
const collected = [];
|
|
956
|
+
for (const route of routes) {
|
|
957
|
+
for (const [statusCode, responseSchema] of Object.entries(
|
|
958
|
+
route.responses
|
|
959
|
+
)) {
|
|
960
|
+
if (!responseSchema) continue;
|
|
961
|
+
const isSuccess = statusCode.startsWith("2");
|
|
962
|
+
const suffix = isSuccess ? `${statusCode}Response` : `${statusCode}ErrorResponse`;
|
|
963
|
+
const responseSchemaName = generateRouteSchemaName2(
|
|
964
|
+
route.path,
|
|
965
|
+
route.method,
|
|
966
|
+
suffix
|
|
967
|
+
);
|
|
968
|
+
collected.push({ name: responseSchemaName, schema: responseSchema });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return collected;
|
|
972
|
+
}
|
|
783
973
|
var openApiToZodTsCode = (openapi, customImportLines, options) => {
|
|
784
974
|
const components = openapi["components"];
|
|
785
975
|
const schemas = components?.["schemas"] ?? {};
|
|
@@ -792,6 +982,7 @@ var openApiToZodTsCode = (openapi, customImportLines, options) => {
|
|
|
792
982
|
lines.push("import { z } from 'zod';");
|
|
793
983
|
lines.push(...customImportLines ?? []);
|
|
794
984
|
lines.push("");
|
|
985
|
+
const registry = createSchemaRegistry();
|
|
795
986
|
const sortedSchemaNames = topologicalSortSchemas(schemas);
|
|
796
987
|
for (const name of sortedSchemaNames) {
|
|
797
988
|
const schema = schemas[name];
|
|
@@ -802,19 +993,37 @@ var openApiToZodTsCode = (openapi, customImportLines, options) => {
|
|
|
802
993
|
lines.push(`export const ${schemaName} = ${zodExpr};`);
|
|
803
994
|
lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
|
|
804
995
|
lines.push("");
|
|
996
|
+
const fingerprint = getSchemaFingerprint(schema);
|
|
997
|
+
preRegisterSchema(registry, schemaName, fingerprint);
|
|
805
998
|
}
|
|
806
999
|
}
|
|
807
1000
|
if (options?.includeRoutes) {
|
|
808
1001
|
const routes = parseOpenApiPaths(openapi);
|
|
809
1002
|
if (routes.length > 0) {
|
|
810
|
-
const
|
|
1003
|
+
const routeSchemaList = collectRouteSchemas(routes);
|
|
1004
|
+
const commonSchemas = findCommonSchemas(routeSchemaList, 2);
|
|
1005
|
+
if (commonSchemas.length > 0) {
|
|
1006
|
+
lines.push("// Common Error Schemas (deduplicated)");
|
|
1007
|
+
for (const common of commonSchemas) {
|
|
1008
|
+
const zodExpr = convertSchemaToZodString(common.schema);
|
|
1009
|
+
lines.push(`export const ${common.name} = ${zodExpr};`);
|
|
1010
|
+
preRegisterSchema(registry, common.name, common.fingerprint);
|
|
1011
|
+
}
|
|
1012
|
+
lines.push("");
|
|
1013
|
+
}
|
|
1014
|
+
const { declarations, schemaNameToCanonical } = generateRouteSchemas(
|
|
811
1015
|
routes,
|
|
812
|
-
convertSchemaToZodString
|
|
1016
|
+
convertSchemaToZodString,
|
|
1017
|
+
registry
|
|
813
1018
|
);
|
|
814
|
-
if (
|
|
815
|
-
lines.push(
|
|
1019
|
+
if (declarations.length > 0) {
|
|
1020
|
+
lines.push("// Route Schemas");
|
|
1021
|
+
lines.push(...declarations);
|
|
816
1022
|
lines.push("");
|
|
817
|
-
const requestResponseObjs = generateRequestResponseObjects(
|
|
1023
|
+
const requestResponseObjs = generateRequestResponseObjects(
|
|
1024
|
+
routes,
|
|
1025
|
+
schemaNameToCanonical
|
|
1026
|
+
);
|
|
818
1027
|
lines.push(...requestResponseObjs);
|
|
819
1028
|
}
|
|
820
1029
|
}
|