@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/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 lines = [];
611
- const schemaNames = /* @__PURE__ */ new Set();
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
- if (!schemaNames.has(names.paramsSchemaName)) {
619
- schemaNames.add(names.paramsSchemaName);
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
- lines.push(
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
- if (!schemaNames.has(names.querySchemaName)) {
636
- schemaNames.add(names.querySchemaName);
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
- lines.push(
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
- if (!schemaNames.has(names.headersSchemaName)) {
652
- schemaNames.add(names.headersSchemaName);
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
- lines.push(
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
- if (!schemaNames.has(names.bodySchemaName)) {
668
- schemaNames.add(names.bodySchemaName);
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
- lines.push(`export const ${names.bodySchemaName} = ${zodExpr};`);
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
- if (!schemaNames.has(responseSchemaName)) {
685
- schemaNames.add(responseSchemaName);
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
- lines.push(`export const ${responseSchemaName} = ${zodExpr};`);
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 lines;
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(`params: ${names.paramsSchemaName}`);
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(`headers: ${names.headersSchemaName}`);
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 routeSchemas = generateRouteSchemas(
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 (routeSchemas.length > 0) {
815
- lines.push(...routeSchemas);
1019
+ if (declarations.length > 0) {
1020
+ lines.push("// Route Schemas");
1021
+ lines.push(...declarations);
816
1022
  lines.push("");
817
- const requestResponseObjs = generateRequestResponseObjects(routes);
1023
+ const requestResponseObjs = generateRequestResponseObjects(
1024
+ routes,
1025
+ schemaNameToCanonical
1026
+ );
818
1027
  lines.push(...requestResponseObjs);
819
1028
  }
820
1029
  }