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/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
+ }