@sgrgv/swagger-zod 1.0.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.
Files changed (2) hide show
  1. package/index.js +210 -0
  2. package/package.json +11 -0
package/index.js ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import axios from 'axios';
6
+
7
+ const args = process.argv.slice(2);
8
+ const swaggerUrl = args[0];
9
+ const endpoint = args[1];
10
+
11
+ console.log(`📥 Скачиваю спецификацию из: ${swaggerUrl}`);
12
+
13
+ function getZodTypeFromSchema(schema, schemas, isResponse = false) {
14
+ if (!schema) return 'z.unknown()';
15
+
16
+ const isNullable = schema.nullable === true;
17
+ let zodType;
18
+
19
+ if (schema.type === 'string') {
20
+ if (schema.format === 'date-time') {
21
+ zodType = 'z.iso.datetime({ offset: true })';
22
+ } else {
23
+ zodType = 'z.string()';
24
+ }
25
+ } else if (schema.type === 'integer') {
26
+ zodType = 'z.number().int()';
27
+ } else if (schema.type === 'number') {
28
+ zodType = 'z.number()';
29
+ } else if (schema.type === 'boolean') {
30
+ zodType = 'z.boolean()';
31
+ } else if (schema.type === 'array') {
32
+ const itemType = getZodTypeFromSchema(schema.items, schemas, isResponse);
33
+ zodType = `z.array(${itemType})`;
34
+ } else if (schema.type === 'object' && schema.properties) {
35
+ const props = [];
36
+ const requiredFields = schema.required || [];
37
+
38
+ for (const [key, prop] of Object.entries(schema.properties)) {
39
+ const isRequired = requiredFields.includes(key);
40
+ const propType = getZodTypeFromSchema(prop, schemas, isResponse);
41
+ const keyName = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
42
+
43
+ // Для response не добавляем .optional()
44
+ // Для body добавляем .optional() только если поле не в required
45
+ if (isResponse) {
46
+ props.push(` ${keyName}: ${propType}`);
47
+ } else {
48
+ props.push(` ${keyName}: ${propType}${isRequired ? '' : '.optional()'}`);
49
+ }
50
+ }
51
+ zodType = `z.object({\n${props.join(',\n')}\n })`;
52
+ } else if (schema.$ref) {
53
+ const refName = schema.$ref.split('/').pop();
54
+ const refSchema = schemas[refName];
55
+ if (refSchema) {
56
+ zodType = getZodTypeFromSchema(refSchema, schemas, isResponse);
57
+ } else {
58
+ zodType = 'z.unknown()';
59
+ }
60
+ } else {
61
+ zodType = 'z.unknown()';
62
+ }
63
+
64
+ if (isNullable) {
65
+ return `${zodType}.nullable()`;
66
+ }
67
+ return zodType;
68
+ }
69
+
70
+ function getParameterZodType(schema) {
71
+ if (!schema) return 'z.string()';
72
+
73
+ if (schema.type === 'string') {
74
+ if (schema.format === 'date-time') {
75
+ return 'z.iso.datetime({ offset: true })';
76
+ }
77
+ return 'z.string()';
78
+ }
79
+ if (schema.type === 'integer') return 'z.number().int()';
80
+ if (schema.type === 'number') return 'z.number()';
81
+ if (schema.type === 'boolean') return 'z.boolean()';
82
+ return 'z.string()';
83
+ }
84
+
85
+ function getTypeFromSchema(schema, schemas) {
86
+ if (!schema) return 'unknown';
87
+
88
+ const isNullable = schema.nullable === true;
89
+ let type;
90
+
91
+ if (schema.type === 'string') {
92
+ type = 'string';
93
+ } else if (schema.type === 'integer') {
94
+ type = 'number';
95
+ } else if (schema.type === 'boolean') {
96
+ type = 'boolean';
97
+ } else if (schema.type === 'array') {
98
+ const itemType = getTypeFromSchema(schema.items, schemas);
99
+ type = `Array<${itemType}>`;
100
+ } else if (schema.type === 'object' && schema.properties) {
101
+ const props = [];
102
+ for (const [key, prop] of Object.entries(schema.properties)) {
103
+ const optional = !schema.required?.includes(key) ? '?' : '';
104
+ const propType = getTypeFromSchema(prop, schemas);
105
+ const keyName = /[^a-zA-Z0-9_]/.test(key) ? `"${key}"` : key;
106
+ props.push(` ${keyName}${optional}: ${propType};`);
107
+ }
108
+ type = `{\n${props.join('\n')}\n}`;
109
+ } else if (schema.$ref) {
110
+ const refName = schema.$ref.split('/').pop();
111
+ const refSchema = schemas[refName];
112
+ if (refSchema) {
113
+ type = getTypeFromSchema(refSchema, schemas);
114
+ } else {
115
+ type = 'any';
116
+ }
117
+ } else {
118
+ type = 'any';
119
+ }
120
+
121
+ if (isNullable && type !== 'unknown') {
122
+ return `${type} | null`;
123
+ }
124
+ return type;
125
+ }
126
+
127
+ try {
128
+ const response = await axios.get(swaggerUrl);
129
+ const spec = response.data;
130
+ const schemas = spec.components?.schemas || {};
131
+
132
+ const pathItem = spec.paths[endpoint];
133
+ if (!pathItem) {
134
+ console.log(`❌ Эндпоинт ${endpoint} не найден`);
135
+ process.exit(1);
136
+ }
137
+
138
+ const operation = pathItem.get || pathItem.post;
139
+ if (!operation) {
140
+ console.log(`❌ Метод для ${endpoint} не найден`);
141
+ process.exit(1);
142
+ }
143
+
144
+ const parameters = operation.parameters || [];
145
+ const queryParams = parameters.filter(p => p.in === 'query');
146
+ const headerParams = parameters.filter(p => p.in === 'header');
147
+ const bodyParam = operation.requestBody;
148
+
149
+ const responseSchema = operation.responses?.['200']?.content?.['application/json']?.schema;
150
+ const responseType = getTypeFromSchema(responseSchema, schemas);
151
+ const responseZodType = getZodTypeFromSchema(responseSchema, schemas, true); // isResponse = true
152
+
153
+ const bodyType = bodyParam?.content?.['application/json']?.schema;
154
+ const bodyTypeStr = bodyType ? getTypeFromSchema(bodyType, schemas) : null;
155
+ const bodyZodType = bodyType ? getZodTypeFromSchema(bodyType, schemas, false) : null; // isResponse = false
156
+
157
+ const typeName = endpoint.replace(/\//g, '_').slice(1);
158
+
159
+ let code = `/**
160
+ * Типы и Zod схема для эндпоинта: ${endpoint}
161
+ */
162
+
163
+ import { z } from 'zod';
164
+
165
+ `;
166
+
167
+ // Query параметры (всегда optional)
168
+ if (queryParams.length > 0) {
169
+ code += `export const QueryParamsSchema = z.object({\n`;
170
+ queryParams.forEach(p => {
171
+ const paramType = getParameterZodType(p.schema);
172
+ const isRequired = p.required === true;
173
+ const keyName = /[^a-zA-Z0-9_]/.test(p.name) ? `"${p.name}"` : p.name;
174
+ code += ` ${keyName}: ${paramType}${isRequired ? '' : '.optional()'},\n`;
175
+ });
176
+ code += `});\n\n`;
177
+ code += `export type QueryParams = z.infer<typeof QueryParamsSchema>;\n\n`;
178
+ }
179
+
180
+ // Header параметры (всегда optional)
181
+ if (headerParams.length > 0) {
182
+ code += `export const HeaderParamsSchema = z.object({\n`;
183
+ headerParams.forEach(p => {
184
+ const paramType = getParameterZodType(p.schema);
185
+ const isRequired = p.required === true;
186
+ const keyName = /[^a-zA-Z0-9_]/.test(p.name) ? `"${p.name}"` : p.name;
187
+ code += ` ${keyName}: ${paramType}${isRequired ? '' : '.optional()'},\n`;
188
+ });
189
+ code += `});\n\n`;
190
+ code += `export type HeaderParams = z.infer<typeof HeaderParamsSchema>;\n\n`;
191
+ }
192
+
193
+ // Body (optional только если поле не в required)
194
+ if (bodyZodType) {
195
+ code += `export const BodySchema = ${bodyZodType};\n\n`;
196
+ code += `export type Body = z.infer<typeof BodySchema>;\n\n`;
197
+ }
198
+
199
+ // Response (НИКОГДА не добавляем optional)
200
+ code += `export const ResponseSchema = ${responseZodType};\n\n`;
201
+ code += `export type Response = z.infer<typeof ResponseSchema>;\n`;
202
+
203
+ const outputFile = path.join(process.cwd(), `${typeName}.zod.ts`);
204
+ fs.writeFileSync(outputFile, code);
205
+ console.log(`✅ Создан ${typeName}.zod.ts`);
206
+
207
+ } catch (error) {
208
+ console.error('❌ Ошибка:', error.message);
209
+ process.exit(1);
210
+ }
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@sgrgv/swagger-zod",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "swagger-zod": "./index.js"
7
+ },
8
+ "dependencies": {
9
+ "axios": "^1.13.6"
10
+ }
11
+ }