jszy-swagger-doc-generator 1.1.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/OpenAPI.md +298 -0
- package/README.md +193 -0
- package/__tests__/index.test.ts +152 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +154 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +489 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/cli.ts +122 -0
- package/src/index.ts +584 -0
- package/tsconfig.json +23 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { SwaggerDocGenerator, SwaggerDoc } from './index';
|
|
4
|
+
import yargs from 'yargs';
|
|
5
|
+
import { hideBin } from 'yargs/helpers';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
|
|
8
|
+
const argv = yargs(hideBin(process.argv))
|
|
9
|
+
.usage('Usage: $0 [options]')
|
|
10
|
+
.option('url', {
|
|
11
|
+
alias: 'u',
|
|
12
|
+
describe: 'URL to the Swagger JSON file',
|
|
13
|
+
type: 'string'
|
|
14
|
+
})
|
|
15
|
+
.option('input', {
|
|
16
|
+
alias: 'i',
|
|
17
|
+
describe: 'Path to the local Swagger JSON file',
|
|
18
|
+
type: 'string'
|
|
19
|
+
})
|
|
20
|
+
.option('output', {
|
|
21
|
+
alias: 'o',
|
|
22
|
+
describe: 'Output path for the generated documentation (default: ./generated/docs/api-documentation.md)',
|
|
23
|
+
type: 'string',
|
|
24
|
+
default: './generated/docs/api-documentation.md'
|
|
25
|
+
})
|
|
26
|
+
.option('generate-types', {
|
|
27
|
+
describe: 'Generate TypeScript type definitions',
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
default: false
|
|
30
|
+
})
|
|
31
|
+
.option('generate-hooks', {
|
|
32
|
+
describe: 'Generate React hooks',
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
default: false
|
|
35
|
+
})
|
|
36
|
+
.option('types-output', {
|
|
37
|
+
describe: 'Output directory for TypeScript types (default: ./generated/types)',
|
|
38
|
+
type: 'string',
|
|
39
|
+
default: './generated/types'
|
|
40
|
+
})
|
|
41
|
+
.option('hooks-output', {
|
|
42
|
+
describe: 'Output directory for React hooks (default: ./generated/hooks)',
|
|
43
|
+
type: 'string',
|
|
44
|
+
default: './generated/hooks'
|
|
45
|
+
})
|
|
46
|
+
.check((argv) => {
|
|
47
|
+
if (!argv.url && !argv.input) {
|
|
48
|
+
throw new Error('Either --url or --input must be provided');
|
|
49
|
+
}
|
|
50
|
+
if (argv.url && argv.input) {
|
|
51
|
+
throw new Error('Only one of --url or --input can be provided');
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
})
|
|
55
|
+
.help()
|
|
56
|
+
.parseSync();
|
|
57
|
+
|
|
58
|
+
async function run(): Promise<void> {
|
|
59
|
+
try {
|
|
60
|
+
// Create the generated directory if it doesn't exist
|
|
61
|
+
const generatedDir = './generated';
|
|
62
|
+
if (!fs.existsSync(generatedDir)) {
|
|
63
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
64
|
+
console.log(`Created directory: ${generatedDir}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const generator = new SwaggerDocGenerator();
|
|
68
|
+
let swaggerDoc: SwaggerDoc | undefined;
|
|
69
|
+
|
|
70
|
+
if (argv.url) {
|
|
71
|
+
console.log(`Fetching Swagger JSON from: ${argv.url}`);
|
|
72
|
+
swaggerDoc = await generator.fetchSwaggerJSON(argv.url);
|
|
73
|
+
} else if (argv.input) {
|
|
74
|
+
console.log(`Loading Swagger JSON from: ${argv.input}`);
|
|
75
|
+
if (!fs.existsSync(argv.input)) {
|
|
76
|
+
throw new Error(`Input file does not exist: ${argv.input}`);
|
|
77
|
+
}
|
|
78
|
+
swaggerDoc = generator.loadSwaggerFromFile(argv.input);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// This shouldn't happen due to the validation check, but TypeScript doesn't know that
|
|
82
|
+
if (!swaggerDoc) {
|
|
83
|
+
throw new Error('No swagger document loaded');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if we need to generate types
|
|
87
|
+
if (argv.generateTypes) {
|
|
88
|
+
console.log('Generating TypeScript type definitions...');
|
|
89
|
+
const types = generator.generateTypeDefinitions(swaggerDoc);
|
|
90
|
+
const typesOutputPath = argv.typesOutput.endsWith('.ts') ? argv.typesOutput :
|
|
91
|
+
`${argv.typesOutput}/${swaggerDoc.info.title.replace(/\s+/g, '_')}_types.ts`;
|
|
92
|
+
generator.saveTypesToFile(types, typesOutputPath);
|
|
93
|
+
console.log(`Type definitions generated successfully at: ${typesOutputPath}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if we need to generate hooks
|
|
97
|
+
if (argv.generateHooks) {
|
|
98
|
+
console.log('Generating React hooks...');
|
|
99
|
+
const hooksByTag = generator.generateReactHooks(swaggerDoc);
|
|
100
|
+
generator.saveHooksByTag(hooksByTag, argv.hooksOutput);
|
|
101
|
+
console.log(`React hooks generated successfully in: ${argv.hooksOutput}/`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Generate documentation if not generating types or hooks (for backward compatibility)
|
|
105
|
+
if (!argv.generateTypes && !argv.generateHooks) {
|
|
106
|
+
console.log('Generating documentation...');
|
|
107
|
+
const documentation = generator.generateDocumentation(swaggerDoc);
|
|
108
|
+
generator.saveDocumentationToFile(documentation, argv.output);
|
|
109
|
+
console.log(`Documentation generated successfully at: ${argv.output}`);
|
|
110
|
+
}
|
|
111
|
+
} catch (error: unknown) {
|
|
112
|
+
if (error instanceof Error) {
|
|
113
|
+
console.error('Error:', error.message);
|
|
114
|
+
} else {
|
|
115
|
+
console.error('Error:', String(error));
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Run the CLI
|
|
122
|
+
run();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import axios, { AxiosResponse } from 'axios';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface SwaggerDoc {
|
|
6
|
+
swagger?: string;
|
|
7
|
+
openapi?: string;
|
|
8
|
+
info: {
|
|
9
|
+
title: string;
|
|
10
|
+
version: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
};
|
|
13
|
+
paths: {
|
|
14
|
+
[key: string]: {
|
|
15
|
+
[method: string]: {
|
|
16
|
+
summary?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
tags?: string[];
|
|
19
|
+
operationId?: string;
|
|
20
|
+
parameters?: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
in: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
schema?: {
|
|
26
|
+
type?: string;
|
|
27
|
+
format?: string;
|
|
28
|
+
enum?: string[];
|
|
29
|
+
$ref?: string;
|
|
30
|
+
items?: any;
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
requestBody?: {
|
|
35
|
+
content: {
|
|
36
|
+
[contentType: string]: {
|
|
37
|
+
schema: any;
|
|
38
|
+
example?: any;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
responses: {
|
|
43
|
+
[statusCode: string]: {
|
|
44
|
+
description: string;
|
|
45
|
+
content?: {
|
|
46
|
+
[contentType: string]: {
|
|
47
|
+
schema: any;
|
|
48
|
+
example?: any;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
components?: {
|
|
57
|
+
schemas: {
|
|
58
|
+
[key: string]: any;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface Parameter {
|
|
64
|
+
name: string;
|
|
65
|
+
in: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
required?: boolean;
|
|
68
|
+
schema?: {
|
|
69
|
+
type?: string;
|
|
70
|
+
format?: string;
|
|
71
|
+
enum?: string[];
|
|
72
|
+
$ref?: string;
|
|
73
|
+
items?: any;
|
|
74
|
+
[key: string]: any;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Transforms a string to PascalCase
|
|
80
|
+
*/
|
|
81
|
+
function toPascalCase(str: string): string {
|
|
82
|
+
return str
|
|
83
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
84
|
+
return index === 0 ? word.toUpperCase() : word.toUpperCase();
|
|
85
|
+
})
|
|
86
|
+
.replace(/\s+/g, '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Transforms a string to camelCase
|
|
91
|
+
*/
|
|
92
|
+
function toCamelCase(str: string): string {
|
|
93
|
+
return str
|
|
94
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
95
|
+
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
|
96
|
+
})
|
|
97
|
+
.replace(/\s+/g, '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Converts OpenAPI types to TypeScript types
|
|
102
|
+
*/
|
|
103
|
+
function convertTypeToTs(typeDef: any, schemaComponents: { [key: string]: any }): string {
|
|
104
|
+
if (!typeDef) return 'any';
|
|
105
|
+
|
|
106
|
+
if (typeDef.$ref) {
|
|
107
|
+
// Extract the type name from the reference
|
|
108
|
+
const refTypeName = typeDef.$ref.split('/').pop();
|
|
109
|
+
return refTypeName || 'any';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle allOf (used for composition/references)
|
|
113
|
+
if (typeDef.allOf && Array.isArray(typeDef.allOf)) {
|
|
114
|
+
return typeDef.allOf.map((item: any) => {
|
|
115
|
+
if (item.$ref) {
|
|
116
|
+
return item.$ref.split('/').pop();
|
|
117
|
+
} else if (item.type) {
|
|
118
|
+
return convertTypeToTs(item, schemaComponents);
|
|
119
|
+
}
|
|
120
|
+
return 'any';
|
|
121
|
+
}).filter(Boolean).join(' & ') || 'any';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(typeDef.type)) {
|
|
125
|
+
// Handle union types like ["string", "null"]
|
|
126
|
+
if (typeDef.type.includes('null')) {
|
|
127
|
+
const nonNullType = typeDef.type.find((t: string) => t !== 'null');
|
|
128
|
+
return `${convertTypeToTs({...typeDef, type: nonNullType}, schemaComponents)} | null`;
|
|
129
|
+
}
|
|
130
|
+
return 'any';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
switch (typeDef.type) {
|
|
134
|
+
case 'string':
|
|
135
|
+
if (typeDef.enum) {
|
|
136
|
+
return `"${typeDef.enum.join('" | "')}"`;
|
|
137
|
+
}
|
|
138
|
+
if (typeDef.format === 'date' || typeDef.format === 'date-time') {
|
|
139
|
+
return 'string';
|
|
140
|
+
}
|
|
141
|
+
return 'string';
|
|
142
|
+
case 'integer':
|
|
143
|
+
case 'number':
|
|
144
|
+
return 'number';
|
|
145
|
+
case 'boolean':
|
|
146
|
+
return 'boolean';
|
|
147
|
+
case 'array':
|
|
148
|
+
if (typeDef.items) {
|
|
149
|
+
return `${convertTypeToTs(typeDef.items, schemaComponents)}[]`;
|
|
150
|
+
}
|
|
151
|
+
return 'any[]';
|
|
152
|
+
case 'object':
|
|
153
|
+
if (typeDef.properties) {
|
|
154
|
+
// Inline object definition
|
|
155
|
+
const fields = Object.entries(typeDef.properties)
|
|
156
|
+
.map(([propName, propSchema]: [string, any]) => {
|
|
157
|
+
const required = typeDef.required && typeDef.required.includes(propName);
|
|
158
|
+
const optional = !required ? '?' : '';
|
|
159
|
+
return ` ${propName}${optional}: ${convertTypeToTs(propSchema, schemaComponents)};`;
|
|
160
|
+
})
|
|
161
|
+
.join('\n');
|
|
162
|
+
return `{\n${fields}\n}`;
|
|
163
|
+
}
|
|
164
|
+
return 'Record<string, any>';
|
|
165
|
+
case 'null':
|
|
166
|
+
return 'null';
|
|
167
|
+
default:
|
|
168
|
+
return 'any';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export class SwaggerDocGenerator {
|
|
173
|
+
/**
|
|
174
|
+
* Fetches the Swagger/OpenAPI JSON from a given URL
|
|
175
|
+
*/
|
|
176
|
+
async fetchSwaggerJSON(url: string): Promise<SwaggerDoc> {
|
|
177
|
+
try {
|
|
178
|
+
const response: AxiosResponse<SwaggerDoc> = await axios.get(url);
|
|
179
|
+
return response.data;
|
|
180
|
+
} catch (error: unknown) {
|
|
181
|
+
if (error instanceof Error) {
|
|
182
|
+
throw new Error(`Failed to fetch Swagger JSON from ${url}: ${error.message}`);
|
|
183
|
+
} else {
|
|
184
|
+
throw new Error(`Failed to fetch Swagger JSON from ${url}: ${String(error)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Loads Swagger JSON from a local file
|
|
191
|
+
*/
|
|
192
|
+
loadSwaggerFromFile(filePath: string): SwaggerDoc {
|
|
193
|
+
try {
|
|
194
|
+
const jsonData = fs.readFileSync(filePath, 'utf8');
|
|
195
|
+
return JSON.parse(jsonData);
|
|
196
|
+
} catch (error: unknown) {
|
|
197
|
+
if (error instanceof Error) {
|
|
198
|
+
throw new Error(`Failed to load Swagger JSON from ${filePath}: ${error.message}`);
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error(`Failed to load Swagger JSON from ${filePath}: ${String(error)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generates frontend documentation from the Swagger doc
|
|
207
|
+
*/
|
|
208
|
+
generateDocumentation(swaggerDoc: SwaggerDoc): string {
|
|
209
|
+
let documentation = `# ${swaggerDoc.info.title}\n\n`;
|
|
210
|
+
documentation += `${swaggerDoc.info.description || swaggerDoc.info.title} v${swaggerDoc.info.version}\n\n`;
|
|
211
|
+
|
|
212
|
+
// Add API endpoints section
|
|
213
|
+
documentation += "## API Endpoints\n\n";
|
|
214
|
+
|
|
215
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
216
|
+
Object.entries(methods).forEach(([method, endpointInfo]) => {
|
|
217
|
+
documentation += `### ${method.toUpperCase()} ${path}\n\n`;
|
|
218
|
+
|
|
219
|
+
if (endpointInfo.summary) {
|
|
220
|
+
documentation += `**Summary:** ${endpointInfo.summary}\n\n`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (endpointInfo.description) {
|
|
224
|
+
documentation += `**Description:** ${endpointInfo.description}\n\n`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add parameters
|
|
228
|
+
if (endpointInfo.parameters && endpointInfo.parameters.length > 0) {
|
|
229
|
+
documentation += "**Parameters:**\n\n";
|
|
230
|
+
endpointInfo.parameters.forEach(param => {
|
|
231
|
+
documentation += `- **${param.name}** (${param.in}): ${param.description || ''}`;
|
|
232
|
+
if (param.required) {
|
|
233
|
+
documentation += ' *(required)*';
|
|
234
|
+
}
|
|
235
|
+
documentation += '\n';
|
|
236
|
+
});
|
|
237
|
+
documentation += '\n';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add request body if present
|
|
241
|
+
if (endpointInfo.requestBody) {
|
|
242
|
+
documentation += "**Request Body:**\n\n";
|
|
243
|
+
Object.entries(endpointInfo.requestBody.content).forEach(([contentType, contentInfo]) => {
|
|
244
|
+
documentation += `- Content-Type: \`${contentType}\`\n`;
|
|
245
|
+
|
|
246
|
+
if (contentInfo.example) {
|
|
247
|
+
documentation += `\n**Example Request:**\n\`\`\`json\n${JSON.stringify(contentInfo.example, null, 2)}\n\`\`\`\n\n`;
|
|
248
|
+
} else if (contentInfo.schema) {
|
|
249
|
+
documentation += `\n**Schema:**\n\`\`\`json\n${JSON.stringify(contentInfo.schema, null, 2)}\n\`\`\`\n\n`;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add responses
|
|
255
|
+
if (endpointInfo.responses) {
|
|
256
|
+
documentation += "**Responses:**\n\n";
|
|
257
|
+
Object.entries(endpointInfo.responses).forEach(([statusCode, responseInfo]) => {
|
|
258
|
+
documentation += `- **${statusCode}**: ${responseInfo.description}\n`;
|
|
259
|
+
|
|
260
|
+
if (responseInfo.content) {
|
|
261
|
+
Object.entries(responseInfo.content).forEach(([contentType, contentInfo]) => {
|
|
262
|
+
if (contentInfo.example) {
|
|
263
|
+
documentation += `\n**Example Response:**\n\`\`\`json\n${JSON.stringify(contentInfo.example, null, 2)}\n\`\`\`\n\n`;
|
|
264
|
+
} else if (contentInfo.schema) {
|
|
265
|
+
documentation += `\n**Schema:**\n\`\`\`json\n${JSON.stringify(contentInfo.schema, null, 2)}\n\`\`\`\n\n`;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
documentation += "---\n\n";
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return documentation;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generates TypeScript type definitions from the schemas in Swagger doc
|
|
281
|
+
*/
|
|
282
|
+
generateTypeDefinitions(swaggerDoc: SwaggerDoc): string {
|
|
283
|
+
if (!swaggerDoc.components?.schemas) {
|
|
284
|
+
return '';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let typeDefs = '// Auto-generated TypeScript types from Swagger schema\n\n';
|
|
288
|
+
|
|
289
|
+
Object.entries(swaggerDoc.components.schemas).forEach(([typeName, schema]) => {
|
|
290
|
+
typeDefs += this.generateSingleTypeDefinition(typeName, schema, swaggerDoc.components!.schemas);
|
|
291
|
+
typeDefs += '\n';
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return typeDefs;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Generates a single TypeScript type definition
|
|
299
|
+
*/
|
|
300
|
+
generateSingleTypeDefinition(typeName: string, schema: any, allSchemas: { [key: string]: any }): string {
|
|
301
|
+
if (schema.enum) {
|
|
302
|
+
// Enum type
|
|
303
|
+
return `export type ${typeName} = ${schema.enum.map((val: any) => `'${val}'`).join(' | ')};\n`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (schema.oneOf || schema.anyOf || schema.allOf) {
|
|
307
|
+
// Union type or complex type
|
|
308
|
+
const typeOption = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'allOf';
|
|
309
|
+
const types = schema[typeOption].map((item: any) => {
|
|
310
|
+
if (item.$ref) {
|
|
311
|
+
return item.$ref.split('/').pop();
|
|
312
|
+
} else if (item.type) {
|
|
313
|
+
return convertTypeToTs(item, allSchemas);
|
|
314
|
+
} else {
|
|
315
|
+
return 'any';
|
|
316
|
+
}
|
|
317
|
+
}).filter(Boolean);
|
|
318
|
+
return `export type ${typeName} = ${types.join(' | ')};\n`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (schema.type === 'object') {
|
|
322
|
+
// Object type
|
|
323
|
+
let result = `export interface ${typeName} {\n`;
|
|
324
|
+
|
|
325
|
+
if (schema.properties) {
|
|
326
|
+
Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => {
|
|
327
|
+
const required = schema.required && schema.required.includes(propName);
|
|
328
|
+
const optional = !required ? '?' : '';
|
|
329
|
+
|
|
330
|
+
// Add JSDoc comment if available
|
|
331
|
+
if (propSchema.description) {
|
|
332
|
+
result += ` /** ${propSchema.description} */\n`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
result += ` ${propName}${optional}: ${convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
result += '}\n';
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// For other types (string, number, etc.) that might have additional properties
|
|
344
|
+
return `export type ${typeName} = ${convertTypeToTs(schema, allSchemas)};\n`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generates React hooks from the paths in Swagger doc
|
|
349
|
+
*/
|
|
350
|
+
generateReactHooks(swaggerDoc: SwaggerDoc): Map<string, string> {
|
|
351
|
+
const hooksByTag = new Map<string, string>();
|
|
352
|
+
const schemas = swaggerDoc.components?.schemas || {};
|
|
353
|
+
|
|
354
|
+
// Group endpoints by tag
|
|
355
|
+
const endpointsByTag: { [tag: string]: Array<{ path: string, method: string, endpointInfo: any }> } = {};
|
|
356
|
+
|
|
357
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
358
|
+
Object.entries(methods).forEach(([method, endpointInfo]) => {
|
|
359
|
+
// Determine the tag for this endpoint
|
|
360
|
+
const tag = (endpointInfo.tags && endpointInfo.tags[0]) ? endpointInfo.tags[0] : 'General';
|
|
361
|
+
|
|
362
|
+
if (!endpointsByTag[tag]) {
|
|
363
|
+
endpointsByTag[tag] = [];
|
|
364
|
+
}
|
|
365
|
+
endpointsByTag[tag].push({ path, method, endpointInfo });
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Generate hooks for each tag
|
|
370
|
+
Object.entries(endpointsByTag).forEach(([tag, endpoints]) => {
|
|
371
|
+
let tagContent = this.generateHeaderForTag(tag);
|
|
372
|
+
|
|
373
|
+
// Generate all parameter interfaces for this tag first to avoid duplicates
|
|
374
|
+
const allParamInterfaces: string[] = [];
|
|
375
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
376
|
+
const paramInterface = this.generateParamInterface(path, method, endpointInfo, schemas);
|
|
377
|
+
if (paramInterface && !allParamInterfaces.includes(paramInterface)) {
|
|
378
|
+
allParamInterfaces.push(paramInterface);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Add all unique parameter interfaces
|
|
383
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
384
|
+
tagContent += interfaceCode + '\n';
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Generate individual hooks
|
|
388
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
389
|
+
const hookContent = this.generateSingleHookWithUniqueName(path, method, endpointInfo, schemas);
|
|
390
|
+
tagContent += hookContent + '\n';
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
hooksByTag.set(tag, tagContent);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return hooksByTag;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Generates header content for a specific tag
|
|
401
|
+
*/
|
|
402
|
+
generateHeaderForTag(tag: string): string {
|
|
403
|
+
return `// ${toPascalCase(tag)} API Hooks\n\n`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Generates a parameter interface for an API endpoint
|
|
408
|
+
*/
|
|
409
|
+
generateParamInterface(path: string, method: string, endpointInfo: any, schemas: { [key: string]: any }): string {
|
|
410
|
+
if (!endpointInfo.parameters || endpointInfo.parameters.length === 0) {
|
|
411
|
+
return '';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const pathParams = endpointInfo.parameters.filter((p: Parameter) => p.in === 'path');
|
|
415
|
+
const queryParams = endpointInfo.parameters.filter((p: Parameter) => p.in === 'query');
|
|
416
|
+
|
|
417
|
+
if (pathParams.length === 0 && queryParams.length === 0) {
|
|
418
|
+
return '';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Create a unique interface name based on the operation ID
|
|
422
|
+
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
423
|
+
const interfaceName = `${toPascalCase(operationId)}Params`;
|
|
424
|
+
|
|
425
|
+
let paramsInterface = `export interface ${interfaceName} {\n`;
|
|
426
|
+
|
|
427
|
+
// Add path parameters
|
|
428
|
+
pathParams.forEach((param: Parameter) => {
|
|
429
|
+
const required = param.required ? '' : '?';
|
|
430
|
+
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
431
|
+
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Add query parameters
|
|
435
|
+
queryParams.forEach((param: Parameter) => {
|
|
436
|
+
const required = param.required ? '' : '?';
|
|
437
|
+
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
438
|
+
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
paramsInterface += '}\n';
|
|
442
|
+
return paramsInterface;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Generates a single React hook for an API endpoint with unique parameter interface
|
|
447
|
+
*/
|
|
448
|
+
generateSingleHookWithUniqueName(path: string, method: string, endpointInfo: any, schemas: { [key: string]: any }): string {
|
|
449
|
+
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
450
|
+
const hookName = `use${toPascalCase(operationId)}`;
|
|
451
|
+
|
|
452
|
+
// Use unique parameter interface name
|
|
453
|
+
const pathParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'path') || [];
|
|
454
|
+
const queryParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'query') || [];
|
|
455
|
+
|
|
456
|
+
let paramsDeclaration = '';
|
|
457
|
+
let paramsUsage = '{}';
|
|
458
|
+
const hasParams = pathParams.length > 0 || queryParams.length > 0;
|
|
459
|
+
|
|
460
|
+
if (hasParams) {
|
|
461
|
+
const paramInterfaceName = `${toPascalCase(operationId)}Params`;
|
|
462
|
+
paramsDeclaration = `params: ${paramInterfaceName}`;
|
|
463
|
+
paramsUsage = 'params';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Format the path for use in the code (handle path parameters)
|
|
467
|
+
const pathWithParams = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
468
|
+
|
|
469
|
+
// Determine response type
|
|
470
|
+
let responseType = 'any';
|
|
471
|
+
if (endpointInfo.responses && endpointInfo.responses['200']) {
|
|
472
|
+
const responseSchema = endpointInfo.responses['200'].content?.['application/json']?.schema;
|
|
473
|
+
if (responseSchema) {
|
|
474
|
+
responseType = convertTypeToTs(responseSchema, schemas);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Generate request body parameter if needed
|
|
479
|
+
let requestBodyParam = '';
|
|
480
|
+
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
481
|
+
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
482
|
+
if (bodySchema) {
|
|
483
|
+
const bodyType = convertTypeToTs(bodySchema, schemas);
|
|
484
|
+
requestBodyParam = `, body: ${bodyType}`;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Create the hook function
|
|
489
|
+
let hookCode = `export const ${hookName} = () => {\n`;
|
|
490
|
+
hookCode += ` const apiCall = async (${paramsDeclaration}${requestBodyParam ? requestBodyParam : ''}) => {\n`;
|
|
491
|
+
hookCode += ` const path = \`\${process.env.REACT_APP_API_BASE_URL || ''}${pathWithParams}\`;\n`;
|
|
492
|
+
|
|
493
|
+
// Add query parameters
|
|
494
|
+
if (endpointInfo.parameters && endpointInfo.parameters.some((p: Parameter) => p.in === 'query')) {
|
|
495
|
+
hookCode += ` const queryParams = new URLSearchParams();\n`;
|
|
496
|
+
endpointInfo.parameters.forEach((param: Parameter) => {
|
|
497
|
+
if (param.in === 'query') {
|
|
498
|
+
hookCode += ` if (params.${toCamelCase(param.name)}) queryParams.append('${param.name}', params.${toCamelCase(param.name)}.toString());\n`;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
hookCode += ` const queryString = queryParams.toString();\n`;
|
|
502
|
+
hookCode += ` const url = \`\${path}\${queryString ? '?' + queryString : ''}\`;\n`;
|
|
503
|
+
} else {
|
|
504
|
+
hookCode += ` const url = path;\n`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Add fetch options
|
|
508
|
+
hookCode += ` const options: RequestInit = {\n`;
|
|
509
|
+
hookCode += ` method: '${method.toUpperCase()}',\n`;
|
|
510
|
+
|
|
511
|
+
if (requestBodyParam) {
|
|
512
|
+
hookCode += ` headers: {\n 'Content-Type': 'application/json',\n },\n`;
|
|
513
|
+
hookCode += ` body: JSON.stringify(body),\n`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
hookCode += ` };\n\n`;
|
|
517
|
+
hookCode += ` const result = await fetch(url, options);\n`;
|
|
518
|
+
hookCode += ` return result.json() as Promise<${responseType}>;\n`;
|
|
519
|
+
hookCode += ` };\n\n`;
|
|
520
|
+
hookCode += ` return { ${toCamelCase(operationId)}: apiCall };\n`;
|
|
521
|
+
hookCode += `};\n`;
|
|
522
|
+
|
|
523
|
+
return hookCode;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Generates a single React hook for an API endpoint
|
|
528
|
+
*/
|
|
529
|
+
generateSingleHook(path: string, method: string, endpointInfo: any, schemas: { [key: string]: any }): string {
|
|
530
|
+
// This method is kept for backward compatibility
|
|
531
|
+
return this.generateSingleHookWithUniqueName(path, method, endpointInfo, schemas);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Generate operation ID from path and method if not provided
|
|
536
|
+
*/
|
|
537
|
+
generateOperationId(path: string, method: string): string {
|
|
538
|
+
return `${method.toLowerCase()}_${path.replace(/[\/{}]/g, '_')}`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Saves the generated documentation to a file
|
|
543
|
+
*/
|
|
544
|
+
saveDocumentationToFile(documentation: string, outputPath: string): void {
|
|
545
|
+
const dir = path.dirname(outputPath);
|
|
546
|
+
if (!fs.existsSync(dir)) {
|
|
547
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
fs.writeFileSync(outputPath, documentation, 'utf8');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Saves the generated TypeScript types to a file
|
|
555
|
+
*/
|
|
556
|
+
saveTypesToFile(types: string, outputPath: string): void {
|
|
557
|
+
const dir = path.dirname(outputPath);
|
|
558
|
+
if (!fs.existsSync(dir)) {
|
|
559
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
fs.writeFileSync(outputPath, types, 'utf8');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Saves the generated React hooks to files organized by tag
|
|
567
|
+
*/
|
|
568
|
+
saveHooksByTag(hooksByTag: Map<string, string>, outputDir: string): void {
|
|
569
|
+
const dir = outputDir;
|
|
570
|
+
if (!fs.existsSync(dir)) {
|
|
571
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const [tag, content] of hooksByTag) {
|
|
575
|
+
const tagDir = path.join(outputDir, toCamelCase(tag));
|
|
576
|
+
if (!fs.existsSync(tagDir)) {
|
|
577
|
+
fs.mkdirSync(tagDir, { recursive: true });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const fileName = path.join(tagDir, `${toCamelCase(tag)}.hooks.ts`);
|
|
581
|
+
fs.writeFileSync(fileName, content, 'utf8');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|