@alt-stack/zod-openapi 1.0.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.
@@ -0,0 +1,289 @@
1
+ import { topologicalSortSchemas } from "./dependencies";
2
+ import { convertSchemaToZodString } from "./to-zod";
3
+ import type { AnySchema } from "./types/types";
4
+ import {
5
+ parseOpenApiPaths,
6
+ generateRouteSchemaNames,
7
+ type RouteInfo,
8
+ } from "./routes";
9
+
10
+ function generateRouteSchemaName(
11
+ path: string,
12
+ method: string,
13
+ suffix: string,
14
+ ): string {
15
+ const pathParts = path
16
+ .split("/")
17
+ .filter((p) => p)
18
+ .map((p) => {
19
+ if (p.startsWith("{") && p.endsWith("}")) {
20
+ return p.slice(1, -1);
21
+ }
22
+ return p;
23
+ })
24
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
25
+ const methodPrefix = method.charAt(0) + method.slice(1).toLowerCase();
26
+ const parts = [methodPrefix, ...pathParts, suffix];
27
+ return parts.join("");
28
+ }
29
+
30
+ function generateRouteSchemas(
31
+ routes: RouteInfo[],
32
+ convertSchema: (schema: AnySchema) => string,
33
+ ): string[] {
34
+ const lines: string[] = [];
35
+ const schemaNames = new Set<string>();
36
+
37
+ for (const route of routes) {
38
+ const names = generateRouteSchemaNames(route);
39
+ const pathParams = route.parameters.filter((p) => p.in === "path");
40
+ const queryParams = route.parameters.filter((p) => p.in === "query");
41
+ const headerParams = route.parameters.filter((p) => p.in === "header");
42
+
43
+ if (names.paramsSchemaName && pathParams.length > 0) {
44
+ if (!schemaNames.has(names.paramsSchemaName)) {
45
+ schemaNames.add(names.paramsSchemaName);
46
+ const properties: string[] = [];
47
+ const required: string[] = [];
48
+ for (const param of pathParams) {
49
+ const zodExpr = convertSchema(param.schema);
50
+ properties.push(`${param.name}: ${zodExpr}`);
51
+ if (param.required) {
52
+ required.push(param.name);
53
+ }
54
+ }
55
+ lines.push(
56
+ `export const ${names.paramsSchemaName} = z.object({ ${properties.join(", ")} });`,
57
+ );
58
+ }
59
+ }
60
+
61
+ if (names.querySchemaName && queryParams.length > 0) {
62
+ if (!schemaNames.has(names.querySchemaName)) {
63
+ schemaNames.add(names.querySchemaName);
64
+ const properties: string[] = [];
65
+ for (const param of queryParams) {
66
+ let zodExpr = convertSchema(param.schema);
67
+ if (!param.required) {
68
+ zodExpr += ".optional()";
69
+ }
70
+ properties.push(`${param.name}: ${zodExpr}`);
71
+ }
72
+ lines.push(
73
+ `export const ${names.querySchemaName} = z.object({ ${properties.join(", ")} });`,
74
+ );
75
+ }
76
+ }
77
+
78
+ if (names.headersSchemaName && headerParams.length > 0) {
79
+ if (!schemaNames.has(names.headersSchemaName)) {
80
+ schemaNames.add(names.headersSchemaName);
81
+ const properties: string[] = [];
82
+ for (const param of headerParams) {
83
+ let zodExpr = convertSchema(param.schema);
84
+ if (!param.required) {
85
+ zodExpr += ".optional()";
86
+ }
87
+ properties.push(`${param.name}: ${zodExpr}`);
88
+ }
89
+ lines.push(
90
+ `export const ${names.headersSchemaName} = z.object({ ${properties.join(", ")} });`,
91
+ );
92
+ }
93
+ }
94
+
95
+ if (names.bodySchemaName && route.requestBody) {
96
+ if (!schemaNames.has(names.bodySchemaName)) {
97
+ schemaNames.add(names.bodySchemaName);
98
+ const zodExpr = convertSchema(route.requestBody);
99
+ lines.push(`export const ${names.bodySchemaName} = ${zodExpr};`);
100
+ }
101
+ }
102
+
103
+ // Generate schemas for ALL status codes, not just success
104
+ for (const [statusCode, responseSchema] of Object.entries(
105
+ route.responses,
106
+ )) {
107
+ if (!responseSchema) continue;
108
+
109
+ const isSuccess = statusCode.startsWith("2");
110
+ const suffix = isSuccess
111
+ ? `${statusCode}Response`
112
+ : `${statusCode}ErrorResponse`;
113
+ const responseSchemaName = generateRouteSchemaName(
114
+ route.path,
115
+ route.method,
116
+ suffix,
117
+ );
118
+
119
+ if (!schemaNames.has(responseSchemaName)) {
120
+ schemaNames.add(responseSchemaName);
121
+ const zodExpr = convertSchema(responseSchema);
122
+ lines.push(`export const ${responseSchemaName} = ${zodExpr};`);
123
+ }
124
+ }
125
+ }
126
+
127
+ return lines;
128
+ }
129
+
130
+ function generateRequestResponseObjects(routes: RouteInfo[]): string[] {
131
+ const lines: string[] = [];
132
+ const requestPaths: Record<string, Record<string, string[]>> = {};
133
+ const responsePaths: Record<
134
+ string,
135
+ Record<string, Record<string, string>>
136
+ > = {};
137
+
138
+ for (const route of routes) {
139
+ const names = generateRouteSchemaNames(route);
140
+ const pathParams = route.parameters.filter((p) => p.in === "path");
141
+ const queryParams = route.parameters.filter((p) => p.in === "query");
142
+ const headerParams = route.parameters.filter((p) => p.in === "header");
143
+
144
+ if (!requestPaths[route.path]) {
145
+ requestPaths[route.path] = {};
146
+ }
147
+ const requestMethodObj = requestPaths[route.path]!;
148
+ if (!requestMethodObj[route.method]) {
149
+ requestMethodObj[route.method] = [];
150
+ }
151
+
152
+ const requestParts: string[] = [];
153
+ if (names.paramsSchemaName && pathParams.length > 0) {
154
+ requestParts.push(`params: ${names.paramsSchemaName}`);
155
+ }
156
+ if (names.querySchemaName && queryParams.length > 0) {
157
+ requestParts.push(`query: ${names.querySchemaName}`);
158
+ }
159
+ if (names.headersSchemaName && headerParams.length > 0) {
160
+ requestParts.push(`headers: ${names.headersSchemaName}`);
161
+ }
162
+ if (names.bodySchemaName && route.requestBody) {
163
+ requestParts.push(`body: ${names.bodySchemaName}`);
164
+ }
165
+
166
+ if (requestParts.length > 0) {
167
+ requestMethodObj[route.method] = requestParts;
168
+ }
169
+
170
+ // Store all status codes in nested structure
171
+ if (!responsePaths[route.path]) {
172
+ responsePaths[route.path] = {};
173
+ }
174
+ const responseMethodObj = responsePaths[route.path]!;
175
+ if (!responseMethodObj[route.method]) {
176
+ responseMethodObj[route.method] = {};
177
+ }
178
+
179
+ for (const [statusCode, responseSchema] of Object.entries(
180
+ route.responses,
181
+ )) {
182
+ if (!responseSchema) continue;
183
+
184
+ const isSuccess = statusCode.startsWith("2");
185
+ const suffix = isSuccess
186
+ ? `${statusCode}Response`
187
+ : `${statusCode}ErrorResponse`;
188
+ const responseSchemaName = generateRouteSchemaName(
189
+ route.path,
190
+ route.method,
191
+ suffix,
192
+ );
193
+ responseMethodObj[route.method]![statusCode] = responseSchemaName;
194
+ }
195
+ }
196
+
197
+ lines.push("export const Request = {");
198
+ for (const [path, methods] of Object.entries(requestPaths)) {
199
+ const methodEntries = Object.entries(methods).filter(
200
+ ([, parts]) => parts.length > 0,
201
+ );
202
+ if (methodEntries.length > 0) {
203
+ lines.push(` '${path}': {`);
204
+ for (const [method, parts] of methodEntries) {
205
+ lines.push(` ${method}: {`);
206
+ for (const part of parts) {
207
+ lines.push(` ${part},`);
208
+ }
209
+ lines.push(` },`);
210
+ }
211
+ lines.push(` },`);
212
+ }
213
+ }
214
+ lines.push("} as const;");
215
+ lines.push("");
216
+
217
+ lines.push("export const Response = {");
218
+ for (const [path, methods] of Object.entries(responsePaths)) {
219
+ const methodEntries = Object.entries(methods);
220
+ if (methodEntries.length > 0) {
221
+ lines.push(` '${path}': {`);
222
+ for (const [method, statusCodes] of methodEntries) {
223
+ lines.push(` ${method}: {`);
224
+ for (const [statusCode, schemaName] of Object.entries(statusCodes)) {
225
+ lines.push(` '${statusCode}': ${schemaName},`);
226
+ }
227
+ lines.push(` },`);
228
+ }
229
+ lines.push(` },`);
230
+ }
231
+ }
232
+ lines.push("} as const;");
233
+
234
+ return lines;
235
+ }
236
+
237
+ export const openApiToZodTsCode = (
238
+ openapi: Record<string, unknown>,
239
+ customImportLines?: string[],
240
+ options?: { includeRoutes?: boolean },
241
+ ): string => {
242
+ const components = (openapi as AnySchema)["components"] as
243
+ | AnySchema
244
+ | undefined;
245
+ const schemas: Record<string, AnySchema> =
246
+ (components?.["schemas"] as Record<string, AnySchema>) ?? {};
247
+
248
+ const lines: string[] = [];
249
+ lines.push("/**");
250
+ lines.push(" * This file was automatically generated from OpenAPI schema");
251
+ lines.push(" * Do not manually edit this file");
252
+ lines.push(" */");
253
+ lines.push("");
254
+ lines.push("import { z } from 'zod';");
255
+ lines.push(...(customImportLines ?? []));
256
+ lines.push("");
257
+
258
+ const sortedSchemaNames = topologicalSortSchemas(schemas);
259
+
260
+ for (const name of sortedSchemaNames) {
261
+ const schema = schemas[name];
262
+ if (schema) {
263
+ const zodExpr = convertSchemaToZodString(schema);
264
+ const schemaName = `${name}Schema`;
265
+ const typeName = name;
266
+ lines.push(`export const ${schemaName} = ${zodExpr};`);
267
+ lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
268
+ lines.push("");
269
+ }
270
+ }
271
+
272
+ if (options?.includeRoutes) {
273
+ const routes = parseOpenApiPaths(openapi);
274
+ if (routes.length > 0) {
275
+ const routeSchemas = generateRouteSchemas(
276
+ routes,
277
+ convertSchemaToZodString,
278
+ );
279
+ if (routeSchemas.length > 0) {
280
+ lines.push(...routeSchemas);
281
+ lines.push("");
282
+ const requestResponseObjs = generateRequestResponseObjects(routes);
283
+ lines.push(...requestResponseObjs);
284
+ }
285
+ }
286
+ }
287
+
288
+ return lines.join("\n");
289
+ };