jszy-swagger-doc-generator 1.4.0 → 1.5.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/README.md +327 -113
- package/dist/cli.js +34 -1
- package/dist/cli.js.map +1 -1
- package/dist/generators/swagger.generator.d.ts +90 -0
- package/dist/generators/swagger.generator.js +626 -0
- package/dist/generators/swagger.generator.js.map +1 -0
- package/dist/helpers/handlebars.helpers.d.ts +4 -0
- package/dist/helpers/handlebars.helpers.js +92 -0
- package/dist/helpers/handlebars.helpers.js.map +1 -0
- package/dist/helpers/string.helpers.d.ts +8 -0
- package/dist/helpers/string.helpers.js +25 -0
- package/dist/helpers/string.helpers.js.map +1 -0
- package/dist/helpers/template.helpers.d.ts +4 -0
- package/dist/helpers/template.helpers.js +105 -0
- package/dist/helpers/template.helpers.js.map +1 -0
- package/dist/helpers/type.helpers.d.ts +18 -0
- package/dist/helpers/type.helpers.js +229 -0
- package/dist/helpers/type.helpers.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +432 -159
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +97 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +35 -1
- package/src/generators/swagger.generator.ts +664 -0
- package/src/helpers/template.helpers.ts +72 -0
- package/src/helpers/type.helpers.ts +232 -0
- package/src/index.ts +465 -162
- package/src/types.ts +98 -0
- package/templates/hooks/individual-hook.hbs +55 -0
- package/templates/hooks/react-hook.hbs +14 -0
- package/templates/types/type-definition.hbs +5 -0
- package/test-openapi-swagger.json +454 -0
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import axios, { AxiosResponse } from 'axios';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { compileTemplate } from './helpers/template.helpers';
|
|
5
|
+
import { generateSingleTypeDefinition } from './helpers/type.helpers';
|
|
4
6
|
|
|
5
7
|
export interface SwaggerDoc {
|
|
6
8
|
swagger?: string;
|
|
@@ -97,79 +99,28 @@ function toCamelCase(str: string): string {
|
|
|
97
99
|
.replace(/\s+/g, '');
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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';
|
|
102
|
+
export class SwaggerDocGenerator {
|
|
103
|
+
/**
|
|
104
|
+
* Transforms a string to PascalCase
|
|
105
|
+
*/
|
|
106
|
+
private toPascalCase(str: string): string {
|
|
107
|
+
return str
|
|
108
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
109
|
+
return index === 0 ? word.toUpperCase() : word.toUpperCase();
|
|
110
|
+
})
|
|
111
|
+
.replace(/\s+/g, '');
|
|
131
112
|
}
|
|
132
113
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
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';
|
|
114
|
+
/**
|
|
115
|
+
* Transforms a string to camelCase
|
|
116
|
+
*/
|
|
117
|
+
private toCamelCase(str: string): string {
|
|
118
|
+
return str
|
|
119
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
120
|
+
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
|
121
|
+
})
|
|
122
|
+
.replace(/\s+/g, '');
|
|
169
123
|
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export class SwaggerDocGenerator {
|
|
173
124
|
/**
|
|
174
125
|
* Fetches the Swagger/OpenAPI JSON from a given URL
|
|
175
126
|
*/
|
|
@@ -294,6 +245,151 @@ export class SwaggerDocGenerator {
|
|
|
294
245
|
return typeDefs;
|
|
295
246
|
}
|
|
296
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Generates React hooks from the paths in Swagger doc organized by tag
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Converts OpenAPI types to TypeScript types
|
|
254
|
+
*/
|
|
255
|
+
private convertTypeToTs(typeDef: any, schemaComponents: { [key: string]: any }): string {
|
|
256
|
+
if (!typeDef) return 'any';
|
|
257
|
+
|
|
258
|
+
if (typeDef.$ref) {
|
|
259
|
+
// Extract the type name from the reference
|
|
260
|
+
const refTypeName = typeDef.$ref.split('/').pop();
|
|
261
|
+
return refTypeName || 'any';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Handle allOf (used for composition/references) - combine all properties
|
|
265
|
+
if (typeDef.allOf && Array.isArray(typeDef.allOf)) {
|
|
266
|
+
const combinedProperties: any = {};
|
|
267
|
+
const refTypes: string[] = [];
|
|
268
|
+
|
|
269
|
+
for (const item of typeDef.allOf) {
|
|
270
|
+
if (item.$ref) {
|
|
271
|
+
// Extract the type name from the reference
|
|
272
|
+
const refTypeName = item.$ref.split('/').pop();
|
|
273
|
+
if (refTypeName) {
|
|
274
|
+
refTypes.push(refTypeName);
|
|
275
|
+
}
|
|
276
|
+
} else if (item.type === 'object' && item.properties) {
|
|
277
|
+
// Combine properties from inline object definitions
|
|
278
|
+
Object.assign(combinedProperties, item.properties);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (refTypes.length > 0 && Object.keys(combinedProperties).length > 0) {
|
|
283
|
+
// We have both references and inline properties
|
|
284
|
+
const inlineDef = {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: combinedProperties,
|
|
287
|
+
required: typeDef.required
|
|
288
|
+
};
|
|
289
|
+
const inlineType = this.convertTypeToTs(inlineDef, schemaComponents);
|
|
290
|
+
return `${refTypes.join(' & ')} & ${inlineType}`;
|
|
291
|
+
} else if (refTypes.length > 0) {
|
|
292
|
+
// Only references
|
|
293
|
+
return refTypes.join(' & ');
|
|
294
|
+
} else if (Object.keys(combinedProperties).length > 0) {
|
|
295
|
+
// Only inline properties
|
|
296
|
+
return this.convertTypeToTs({
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: combinedProperties,
|
|
299
|
+
required: typeDef.required
|
|
300
|
+
}, schemaComponents);
|
|
301
|
+
} else {
|
|
302
|
+
return 'any';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Handle oneOf (union types)
|
|
307
|
+
if (typeDef.oneOf && Array.isArray(typeDef.oneOf)) {
|
|
308
|
+
return typeDef.oneOf.map((item: any) => {
|
|
309
|
+
if (item.$ref) {
|
|
310
|
+
return item.$ref.split('/').pop();
|
|
311
|
+
} else if (item.type) {
|
|
312
|
+
return this.convertTypeToTs(item, schemaComponents);
|
|
313
|
+
}
|
|
314
|
+
return 'any';
|
|
315
|
+
}).filter(Boolean).join(' | ') || 'any';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Handle anyOf (union types)
|
|
319
|
+
if (typeDef.anyOf && Array.isArray(typeDef.anyOf)) {
|
|
320
|
+
return typeDef.anyOf.map((item: any) => {
|
|
321
|
+
if (item.$ref) {
|
|
322
|
+
return item.$ref.split('/').pop();
|
|
323
|
+
} else if (item.type) {
|
|
324
|
+
return this.convertTypeToTs(item, schemaComponents);
|
|
325
|
+
}
|
|
326
|
+
return 'any';
|
|
327
|
+
}).filter(Boolean).join(' | ') || 'any';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (Array.isArray(typeDef.type)) {
|
|
331
|
+
// Handle union types like ["string", "null"]
|
|
332
|
+
if (typeDef.type.includes('null')) {
|
|
333
|
+
const nonNullTypes = typeDef.type.filter((t: string) => t !== 'null');
|
|
334
|
+
if (nonNullTypes.length === 1) {
|
|
335
|
+
return `${this.convertTypeToTs({...typeDef, type: nonNullTypes[0]}, schemaComponents)} | null`;
|
|
336
|
+
} else {
|
|
337
|
+
// Handle complex union types with null
|
|
338
|
+
const nonNullTypeStr = nonNullTypes
|
|
339
|
+
.map((t: string) => this.convertTypeToTs({...typeDef, type: t}, schemaComponents))
|
|
340
|
+
.join(' | ');
|
|
341
|
+
return `${nonNullTypeStr} | null`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Handle other array type unions
|
|
345
|
+
return typeDef.type
|
|
346
|
+
.map((t: string) => this.convertTypeToTs({...typeDef, type: t}, schemaComponents))
|
|
347
|
+
.join(' | ') || 'any';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
switch (typeDef.type) {
|
|
351
|
+
case 'string':
|
|
352
|
+
if (typeDef.enum) {
|
|
353
|
+
return `"${typeDef.enum.join('" | "')}"`;
|
|
354
|
+
}
|
|
355
|
+
if (typeDef.format === 'date' || typeDef.format === 'date-time') {
|
|
356
|
+
return 'string';
|
|
357
|
+
}
|
|
358
|
+
return 'string';
|
|
359
|
+
case 'integer':
|
|
360
|
+
case 'number':
|
|
361
|
+
return 'number';
|
|
362
|
+
case 'boolean':
|
|
363
|
+
return 'boolean';
|
|
364
|
+
case 'array':
|
|
365
|
+
if (typeDef.items) {
|
|
366
|
+
return `${this.convertTypeToTs(typeDef.items, schemaComponents)}[]`;
|
|
367
|
+
}
|
|
368
|
+
return 'any[]';
|
|
369
|
+
case 'object':
|
|
370
|
+
if (typeDef.properties) {
|
|
371
|
+
// Inline object definition
|
|
372
|
+
const fields = Object.entries(typeDef.properties)
|
|
373
|
+
.map(([propName, propSchema]: [string, any]) => {
|
|
374
|
+
const required = typeDef.required && typeDef.required.includes(propName);
|
|
375
|
+
const optional = !required ? '?' : '';
|
|
376
|
+
const type = this.convertTypeToTs(propSchema, schemaComponents);
|
|
377
|
+
// Get the description for JSDoc if available
|
|
378
|
+
const propDescription = propSchema.description || propSchema.title;
|
|
379
|
+
const jsDoc = propDescription ? ` /** ${propDescription} */\n` : '';
|
|
380
|
+
return `${jsDoc} ${propName}${optional}: ${type};`;
|
|
381
|
+
})
|
|
382
|
+
.join('\n');
|
|
383
|
+
return `{\n${fields}\n }`;
|
|
384
|
+
}
|
|
385
|
+
return 'Record<string, any>';
|
|
386
|
+
case 'null':
|
|
387
|
+
return 'null';
|
|
388
|
+
default:
|
|
389
|
+
return 'any';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
297
393
|
/**
|
|
298
394
|
* Generates a single TypeScript type definition
|
|
299
395
|
*/
|
|
@@ -303,14 +399,14 @@ export class SwaggerDocGenerator {
|
|
|
303
399
|
return `export type ${typeName} = ${schema.enum.map((val: any) => `'${val}'`).join(' | ')};\n`;
|
|
304
400
|
}
|
|
305
401
|
|
|
306
|
-
if (schema.oneOf || schema.anyOf
|
|
307
|
-
// Union type or complex type
|
|
308
|
-
const typeOption = schema.oneOf ? 'oneOf' :
|
|
402
|
+
if (schema.oneOf || schema.anyOf) {
|
|
403
|
+
// Union type or complex type (oneOf/anyOf)
|
|
404
|
+
const typeOption = schema.oneOf ? 'oneOf' : 'anyOf';
|
|
309
405
|
const types = schema[typeOption].map((item: any) => {
|
|
310
406
|
if (item.$ref) {
|
|
311
407
|
return item.$ref.split('/').pop();
|
|
312
408
|
} else if (item.type) {
|
|
313
|
-
return convertTypeToTs(item, allSchemas);
|
|
409
|
+
return this.convertTypeToTs(item, allSchemas);
|
|
314
410
|
} else {
|
|
315
411
|
return 'any';
|
|
316
412
|
}
|
|
@@ -318,6 +414,24 @@ export class SwaggerDocGenerator {
|
|
|
318
414
|
return `export type ${typeName} = ${types.join(' | ')};\n`;
|
|
319
415
|
}
|
|
320
416
|
|
|
417
|
+
if (schema.allOf) {
|
|
418
|
+
// Handle allOf - composition of multiple schemas
|
|
419
|
+
const allParts: string[] = [];
|
|
420
|
+
for (const part of schema.allOf) {
|
|
421
|
+
if (part.$ref) {
|
|
422
|
+
const refTypeName = part.$ref.split('/').pop();
|
|
423
|
+
if (refTypeName) {
|
|
424
|
+
allParts.push(refTypeName);
|
|
425
|
+
}
|
|
426
|
+
} else if (part.type === 'object' && part.properties) {
|
|
427
|
+
// Create a temporary interface for inline object
|
|
428
|
+
const inlineInterface = this.generateInlineObjectInterface(part, `${typeName}Inline`, allSchemas);
|
|
429
|
+
allParts.push(inlineInterface);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return `export type ${typeName} = ${allParts.join(' & ')};\n`;
|
|
433
|
+
}
|
|
434
|
+
|
|
321
435
|
if (schema.type === 'object') {
|
|
322
436
|
// Object type
|
|
323
437
|
let result = `export interface ${typeName} {\n`;
|
|
@@ -327,12 +441,13 @@ export class SwaggerDocGenerator {
|
|
|
327
441
|
const required = schema.required && schema.required.includes(propName);
|
|
328
442
|
const optional = !required ? '?' : '';
|
|
329
443
|
|
|
330
|
-
// Add JSDoc comment if available
|
|
331
|
-
|
|
332
|
-
|
|
444
|
+
// Add JSDoc comment if available (using title or description)
|
|
445
|
+
const jsDoc = propSchema.title || propSchema.description;
|
|
446
|
+
if (jsDoc) {
|
|
447
|
+
result += ` /** ${jsDoc} */\n`;
|
|
333
448
|
}
|
|
334
449
|
|
|
335
|
-
result += ` ${propName}${optional}: ${convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
450
|
+
result += ` ${propName}${optional}: ${this.convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
336
451
|
});
|
|
337
452
|
}
|
|
338
453
|
|
|
@@ -341,7 +456,30 @@ export class SwaggerDocGenerator {
|
|
|
341
456
|
}
|
|
342
457
|
|
|
343
458
|
// For other types (string, number, etc.) that might have additional properties
|
|
344
|
-
return `export type ${typeName} = ${convertTypeToTs(schema, allSchemas)};\n`;
|
|
459
|
+
return `export type ${typeName} = ${this.convertTypeToTs(schema, allSchemas)};\n`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Generates an inline object interface for allOf composition
|
|
464
|
+
*/
|
|
465
|
+
private generateInlineObjectInterface(schema: any, tempName: string, allSchemas: { [key: string]: any }): string {
|
|
466
|
+
if (!schema.properties) return 'any';
|
|
467
|
+
|
|
468
|
+
let result = '{\n';
|
|
469
|
+
Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => {
|
|
470
|
+
const required = schema.required && schema.required.includes(propName);
|
|
471
|
+
const optional = !required ? '?' : '';
|
|
472
|
+
|
|
473
|
+
// Add JSDoc comment if available (using title or description)
|
|
474
|
+
const jsDoc = propSchema.title || propSchema.description;
|
|
475
|
+
if (jsDoc) {
|
|
476
|
+
result += ` /** ${jsDoc} */\n`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
result += ` ${propName}${optional}: ${this.convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
480
|
+
});
|
|
481
|
+
result += ' }';
|
|
482
|
+
return result;
|
|
345
483
|
}
|
|
346
484
|
|
|
347
485
|
/**
|
|
@@ -657,22 +795,36 @@ export class SwaggerDocGenerator {
|
|
|
657
795
|
|
|
658
796
|
// Create a unique interface name based on the operation ID
|
|
659
797
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
660
|
-
|
|
798
|
+
|
|
799
|
+
// Extract action name from operationId to create cleaner parameter interface names
|
|
800
|
+
// e.g. configController_updateConfig -> UpdateConfigParams instead of ConfigController_updateConfigParams
|
|
801
|
+
let interfaceName: string;
|
|
802
|
+
if (operationId.includes('_')) {
|
|
803
|
+
const parts = operationId.split('_');
|
|
804
|
+
if (parts.length >= 2) {
|
|
805
|
+
// Use just the action part in the interface name
|
|
806
|
+
interfaceName = `${toPascalCase(parts[parts.length - 1])}Params`;
|
|
807
|
+
} else {
|
|
808
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
812
|
+
}
|
|
661
813
|
|
|
662
814
|
let paramsInterface = `export interface ${interfaceName} {\n`;
|
|
663
815
|
|
|
664
816
|
// Add path parameters
|
|
665
817
|
pathParams.forEach((param: Parameter) => {
|
|
666
818
|
const required = param.required ? '' : '?';
|
|
667
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
668
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
819
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
820
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
669
821
|
});
|
|
670
822
|
|
|
671
823
|
// Add query parameters
|
|
672
824
|
queryParams.forEach((param: Parameter) => {
|
|
673
825
|
const required = param.required ? '' : '?';
|
|
674
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
675
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
826
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
827
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
676
828
|
});
|
|
677
829
|
|
|
678
830
|
paramsInterface += '}\n';
|
|
@@ -684,19 +836,42 @@ export class SwaggerDocGenerator {
|
|
|
684
836
|
*/
|
|
685
837
|
generateReactQueryHook(path: string, method: string, endpointInfo: any, schemas: { [key: string]: any }): string {
|
|
686
838
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
687
|
-
|
|
839
|
+
|
|
840
|
+
// Extract action name from operationId to create cleaner hook names
|
|
841
|
+
// e.g. configController_updateConfig -> useUpdateConfig instead of useConfigController_updateConfig
|
|
842
|
+
let hookName = `use${this.toPascalCase(operationId)}`;
|
|
843
|
+
|
|
844
|
+
// Check if operationId follows pattern controller_action and simplify to action
|
|
845
|
+
if (operationId.includes('_')) {
|
|
846
|
+
const parts = operationId.split('_');
|
|
847
|
+
if (parts.length >= 2) {
|
|
848
|
+
// Use just the action part as the hook name
|
|
849
|
+
hookName = `use${this.toPascalCase(parts[parts.length - 1])}`;
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
// For operationIds without underscores, keep the original naming
|
|
853
|
+
hookName = `use${this.toPascalCase(operationId)}`;
|
|
854
|
+
}
|
|
855
|
+
|
|
688
856
|
const hookType = method.toLowerCase() === 'get' ? 'useQuery' : 'useMutation';
|
|
689
857
|
|
|
690
858
|
// Use unique parameter interface name
|
|
691
859
|
const pathParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'path') || [];
|
|
692
860
|
const queryParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'query') || [];
|
|
693
861
|
|
|
694
|
-
// Determine response type
|
|
862
|
+
// Determine response type by checking common success response codes
|
|
695
863
|
let responseType = 'any';
|
|
696
|
-
if (endpointInfo.responses
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
864
|
+
if (endpointInfo.responses) {
|
|
865
|
+
// Check for success responses in order of preference: 200, 201, 204, etc.
|
|
866
|
+
const successCodes = ['200', '201', '204', '202', '203', '205'];
|
|
867
|
+
for (const code of successCodes) {
|
|
868
|
+
if (endpointInfo.responses[code]) {
|
|
869
|
+
const responseSchema = endpointInfo.responses[code].content?.['application/json']?.schema;
|
|
870
|
+
if (responseSchema) {
|
|
871
|
+
responseType = this.convertTypeToTs(responseSchema, schemas);
|
|
872
|
+
break; // Use the first success response found
|
|
873
|
+
}
|
|
874
|
+
}
|
|
700
875
|
}
|
|
701
876
|
}
|
|
702
877
|
|
|
@@ -706,82 +881,42 @@ export class SwaggerDocGenerator {
|
|
|
706
881
|
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
707
882
|
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
708
883
|
if (bodySchema) {
|
|
709
|
-
requestBodyType = convertTypeToTs(bodySchema, schemas);
|
|
884
|
+
requestBodyType = this.convertTypeToTs(bodySchema, schemas);
|
|
710
885
|
hasBody = true;
|
|
711
886
|
}
|
|
712
887
|
}
|
|
713
888
|
|
|
714
|
-
// Format the path for use in the code (handle path parameters)
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
} else {
|
|
747
|
-
// For non-GET requests, use useMutation
|
|
748
|
-
const hasPathParams = pathParams.length > 0;
|
|
749
|
-
if (hasPathParams) {
|
|
750
|
-
const paramInterfaceName = `${toPascalCase(operationId)}Params`;
|
|
751
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
752
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
753
|
-
hookCode += ` return useMutation({\n`;
|
|
754
|
-
hookCode += ` mutationFn: async ({ params, data }: { params: ${paramInterfaceName}; data: ${requestBodyType} }) => {\n`;
|
|
755
|
-
// Format the path for use in the code (handle path parameters)
|
|
756
|
-
let formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
757
|
-
const pathWithParams = `\`\${process.env.REACT_APP_API_BASE_URL || ''}${formattedPath}\``;
|
|
758
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${pathWithParams}, data);\n`;
|
|
759
|
-
hookCode += ` return response.data;\n`;
|
|
760
|
-
hookCode += ` },\n`;
|
|
761
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
762
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
763
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
764
|
-
hookCode += ` },\n`;
|
|
765
|
-
hookCode += ` });\n`;
|
|
766
|
-
hookCode += `};\n`;
|
|
767
|
-
} else {
|
|
768
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
769
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
770
|
-
hookCode += ` return useMutation({\n`;
|
|
771
|
-
hookCode += ` mutationFn: async (data: ${requestBodyType}) => {\n`;
|
|
772
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
773
|
-
hookCode += ` return response.data;\n`;
|
|
774
|
-
hookCode += ` },\n`;
|
|
775
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
776
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
777
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
778
|
-
hookCode += ` },\n`;
|
|
779
|
-
hookCode += ` });\n`;
|
|
780
|
-
hookCode += `};\n`;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
return hookCode;
|
|
889
|
+
// Format the path for use in the code (handle path parameters) - without base URL
|
|
890
|
+
const formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${this.toCamelCase(param)}}`);
|
|
891
|
+
|
|
892
|
+
// Prepare data for the template
|
|
893
|
+
const hookData = {
|
|
894
|
+
hookName: hookName,
|
|
895
|
+
operationId: operationId,
|
|
896
|
+
method: method.toLowerCase(),
|
|
897
|
+
responseType: responseType,
|
|
898
|
+
requestBodyType: requestBodyType,
|
|
899
|
+
hasParams: pathParams.length > 0 || queryParams.length > 0,
|
|
900
|
+
hasPathParams: pathParams.length > 0,
|
|
901
|
+
paramInterfaceName: `${hookName.replace('use', '')}Params`,
|
|
902
|
+
formattedPath: formattedPath,
|
|
903
|
+
isGetRequest: method.toLowerCase() === 'get'
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
// Load and compile the individual hook template
|
|
907
|
+
const fs = require('fs');
|
|
908
|
+
const pathModule = require('path');
|
|
909
|
+
const templatePath = pathModule.join(__dirname, '..', 'templates', 'hooks', 'individual-hook.hbs');
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
const templateSource = fs.readFileSync(templatePath, 'utf8');
|
|
913
|
+
const Handlebars = require('handlebars');
|
|
914
|
+
const template = Handlebars.compile(templateSource);
|
|
915
|
+
return template(hookData);
|
|
916
|
+
} catch (error: any) {
|
|
917
|
+
console.error(`Error reading template file: ${error.message}`);
|
|
918
|
+
return `// Error generating hook for ${operationId}: ${error.message}`;
|
|
919
|
+
}
|
|
785
920
|
}
|
|
786
921
|
|
|
787
922
|
|
|
@@ -906,4 +1041,172 @@ export class SwaggerDocGenerator {
|
|
|
906
1041
|
}
|
|
907
1042
|
}
|
|
908
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Generates frontend resources using Handlebars templates
|
|
1047
|
+
*/
|
|
1048
|
+
generateHandlebarsResources(swaggerDoc: SwaggerDoc, templatePaths: {
|
|
1049
|
+
hooks?: string,
|
|
1050
|
+
types?: string,
|
|
1051
|
+
components?: string,
|
|
1052
|
+
pages?: string
|
|
1053
|
+
} = {}): Map<string, { hooks: string, types: string }> {
|
|
1054
|
+
const resourcesByTag = new Map<string, { hooks: string, types: string }>();
|
|
1055
|
+
const schemas = swaggerDoc.components?.schemas || {};
|
|
1056
|
+
|
|
1057
|
+
// Group endpoints by tag
|
|
1058
|
+
const endpointsByTag: { [tag: string]: Array<{ path: string, method: string, endpointInfo: any }> } = {};
|
|
1059
|
+
|
|
1060
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
1061
|
+
Object.entries(methods).forEach(([method, endpointInfo]: [string, any]) => {
|
|
1062
|
+
// Determine the tag for this endpoint
|
|
1063
|
+
const tag = (endpointInfo.tags && endpointInfo.tags[0]) ? endpointInfo.tags[0] : 'General';
|
|
1064
|
+
|
|
1065
|
+
if (!endpointsByTag[tag]) {
|
|
1066
|
+
endpointsByTag[tag] = [];
|
|
1067
|
+
}
|
|
1068
|
+
endpointsByTag[tag].push({ path, method, endpointInfo });
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// Generate resources for each tag
|
|
1073
|
+
Object.entries(endpointsByTag).forEach(([tag, endpoints]) => {
|
|
1074
|
+
// Prepare context for templates
|
|
1075
|
+
const context: any = {
|
|
1076
|
+
title: swaggerDoc.info.title,
|
|
1077
|
+
description: swaggerDoc.info.description || swaggerDoc.info.title,
|
|
1078
|
+
version: swaggerDoc.info.version,
|
|
1079
|
+
tag: tag,
|
|
1080
|
+
endpoints: endpoints.map(e => ({
|
|
1081
|
+
path: e.path,
|
|
1082
|
+
method: e.method.toUpperCase(),
|
|
1083
|
+
operationId: e.endpointInfo.operationId || this.generateOperationId(e.path, e.method),
|
|
1084
|
+
summary: e.endpointInfo.summary,
|
|
1085
|
+
description: e.endpointInfo.description,
|
|
1086
|
+
parameters: e.endpointInfo.parameters || [],
|
|
1087
|
+
responses: e.endpointInfo.responses,
|
|
1088
|
+
requestBody: e.endpointInfo.requestBody
|
|
1089
|
+
})),
|
|
1090
|
+
schemas: schemas,
|
|
1091
|
+
hasImportTypes: false,
|
|
1092
|
+
usedTypeNames: [] as string[],
|
|
1093
|
+
paramInterfaces: [] as string[],
|
|
1094
|
+
hooks: [] as string[],
|
|
1095
|
+
typeDefinitions: [] as string[]
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// Find types used in this tag
|
|
1099
|
+
const directlyUsedSchemas = new Set<string>();
|
|
1100
|
+
if (schemas) {
|
|
1101
|
+
Object.entries(schemas).forEach(([typeName, schema]) => {
|
|
1102
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
1103
|
+
directlyUsedSchemas.add(typeName);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const allNeededSchemas = this.findAllReferencedSchemas(directlyUsedSchemas, schemas);
|
|
1109
|
+
|
|
1110
|
+
// Generate TypeScript types
|
|
1111
|
+
let typesContent = '';
|
|
1112
|
+
if (schemas) {
|
|
1113
|
+
for (const typeName of allNeededSchemas) {
|
|
1114
|
+
const schema = schemas[typeName];
|
|
1115
|
+
if (schema) {
|
|
1116
|
+
const typeDef = generateSingleTypeDefinition(typeName, schema, schemas);
|
|
1117
|
+
typesContent += typeDef + '\n';
|
|
1118
|
+
context.typeDefinitions.push(typeDef);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Check if there are used types for import
|
|
1124
|
+
if (allNeededSchemas.size > 0) {
|
|
1125
|
+
context.hasImportTypes = true;
|
|
1126
|
+
context.usedTypeNames = Array.from(allNeededSchemas);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Generate parameter interfaces
|
|
1130
|
+
const allParamInterfaces: string[] = [];
|
|
1131
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
1132
|
+
const paramInterface = this.generateParamInterface(path, method, endpointInfo, schemas);
|
|
1133
|
+
if (paramInterface && !allParamInterfaces.includes(paramInterface)) {
|
|
1134
|
+
allParamInterfaces.push(paramInterface);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
context.paramInterfaces = allParamInterfaces;
|
|
1139
|
+
|
|
1140
|
+
// Generate individual hooks
|
|
1141
|
+
const allHooks: string[] = [];
|
|
1142
|
+
const endpointHookContents: string[] = [];
|
|
1143
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
1144
|
+
const hookContent = this.generateReactQueryHook(path, method, endpointInfo, schemas);
|
|
1145
|
+
allHooks.push(hookContent);
|
|
1146
|
+
endpointHookContents.push(hookContent); // Store for template context
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
context.hooks = allHooks;
|
|
1150
|
+
context.endpointHooks = endpointHookContents;
|
|
1151
|
+
|
|
1152
|
+
// Generate resources using specified templates
|
|
1153
|
+
let hooksContent = '';
|
|
1154
|
+
if (templatePaths.hooks) {
|
|
1155
|
+
try {
|
|
1156
|
+
// Add utility functions to context for use in templates
|
|
1157
|
+
context['camelCase'] = (str: string) => this.toCamelCase(str);
|
|
1158
|
+
context['pascalCase'] = (str: string) => this.toPascalCase(str);
|
|
1159
|
+
|
|
1160
|
+
hooksContent = compileTemplate(templatePaths.hooks, context);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
// If template doesn't exist or fails, fall back to default generation
|
|
1163
|
+
console.warn(`Failed to compile hooks template: ${templatePaths.hooks}`, error);
|
|
1164
|
+
// Use the existing method as fallback
|
|
1165
|
+
hooksContent = `// ${this.toPascalCase(tag)} API Hooks\n`;
|
|
1166
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
1167
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
1168
|
+
|
|
1169
|
+
if (context.hasImportTypes) {
|
|
1170
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${this.toCamelCase(tag)}.types';\n\n`;
|
|
1171
|
+
} else {
|
|
1172
|
+
hooksContent += `\n`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
1176
|
+
hooksContent += interfaceCode + '\n';
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
allHooks.forEach(hookCode => {
|
|
1180
|
+
hooksContent += hookCode + '\n';
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
// Default generation if no template is provided
|
|
1185
|
+
hooksContent = `// ${this.toPascalCase(tag)} API Hooks\n`;
|
|
1186
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
1187
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
1188
|
+
|
|
1189
|
+
if (context.hasImportTypes) {
|
|
1190
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${this.toCamelCase(tag)}.types';\n\n`;
|
|
1191
|
+
} else {
|
|
1192
|
+
hooksContent += `\n`;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
1196
|
+
hooksContent += interfaceCode + '\n';
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
allHooks.forEach(hookCode => {
|
|
1200
|
+
hooksContent += hookCode + '\n';
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
resourcesByTag.set(tag, {
|
|
1205
|
+
hooks: hooksContent,
|
|
1206
|
+
types: typesContent
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
return resourcesByTag;
|
|
1211
|
+
}
|
|
909
1212
|
}
|