jszy-swagger-doc-generator 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +403 -158
- 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 +6 -2
- 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 +435 -161
- 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
|
/**
|
|
@@ -678,15 +816,15 @@ export class SwaggerDocGenerator {
|
|
|
678
816
|
// Add path parameters
|
|
679
817
|
pathParams.forEach((param: Parameter) => {
|
|
680
818
|
const required = param.required ? '' : '?';
|
|
681
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
682
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
819
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
820
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
683
821
|
});
|
|
684
822
|
|
|
685
823
|
// Add query parameters
|
|
686
824
|
queryParams.forEach((param: Parameter) => {
|
|
687
825
|
const required = param.required ? '' : '?';
|
|
688
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
689
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
826
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
827
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
690
828
|
});
|
|
691
829
|
|
|
692
830
|
paramsInterface += '}\n';
|
|
@@ -701,18 +839,18 @@ export class SwaggerDocGenerator {
|
|
|
701
839
|
|
|
702
840
|
// Extract action name from operationId to create cleaner hook names
|
|
703
841
|
// e.g. configController_updateConfig -> useUpdateConfig instead of useConfigController_updateConfig
|
|
704
|
-
let hookName = `use${toPascalCase(operationId)}`;
|
|
842
|
+
let hookName = `use${this.toPascalCase(operationId)}`;
|
|
705
843
|
|
|
706
844
|
// Check if operationId follows pattern controller_action and simplify to action
|
|
707
845
|
if (operationId.includes('_')) {
|
|
708
846
|
const parts = operationId.split('_');
|
|
709
847
|
if (parts.length >= 2) {
|
|
710
848
|
// Use just the action part as the hook name
|
|
711
|
-
hookName = `use${toPascalCase(parts[parts.length - 1])}`;
|
|
849
|
+
hookName = `use${this.toPascalCase(parts[parts.length - 1])}`;
|
|
712
850
|
}
|
|
713
851
|
} else {
|
|
714
852
|
// For operationIds without underscores, keep the original naming
|
|
715
|
-
hookName = `use${toPascalCase(operationId)}`;
|
|
853
|
+
hookName = `use${this.toPascalCase(operationId)}`;
|
|
716
854
|
}
|
|
717
855
|
|
|
718
856
|
const hookType = method.toLowerCase() === 'get' ? 'useQuery' : 'useMutation';
|
|
@@ -721,12 +859,19 @@ export class SwaggerDocGenerator {
|
|
|
721
859
|
const pathParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'path') || [];
|
|
722
860
|
const queryParams = endpointInfo.parameters?.filter((p: Parameter) => p.in === 'query') || [];
|
|
723
861
|
|
|
724
|
-
// Determine response type
|
|
862
|
+
// Determine response type by checking common success response codes
|
|
725
863
|
let responseType = 'any';
|
|
726
|
-
if (endpointInfo.responses
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
+
}
|
|
730
875
|
}
|
|
731
876
|
}
|
|
732
877
|
|
|
@@ -736,81 +881,42 @@ export class SwaggerDocGenerator {
|
|
|
736
881
|
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
737
882
|
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
738
883
|
if (bodySchema) {
|
|
739
|
-
requestBodyType = convertTypeToTs(bodySchema, schemas);
|
|
884
|
+
requestBodyType = this.convertTypeToTs(bodySchema, schemas);
|
|
740
885
|
hasBody = true;
|
|
741
886
|
}
|
|
742
887
|
}
|
|
743
888
|
|
|
744
889
|
// Format the path for use in the code (handle path parameters) - without base URL
|
|
745
|
-
const formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
hookCode += `};\n`;
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
// For non-GET requests, use useMutation
|
|
779
|
-
const hasPathParams = pathParams.length > 0;
|
|
780
|
-
if (hasPathParams) {
|
|
781
|
-
// Generate simpler parameter interface name based on hook name instead of operationId
|
|
782
|
-
const paramInterfaceName = `${hookName.replace('use', '')}Params`;
|
|
783
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
784
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
785
|
-
hookCode += ` return useMutation({\n`;
|
|
786
|
-
hookCode += ` mutationFn: async ({ params, data }: { params: ${paramInterfaceName}; data: ${requestBodyType} }) => {\n`;
|
|
787
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
788
|
-
hookCode += ` return response.data;\n`;
|
|
789
|
-
hookCode += ` },\n`;
|
|
790
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
791
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
792
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
793
|
-
hookCode += ` },\n`;
|
|
794
|
-
hookCode += ` });\n`;
|
|
795
|
-
hookCode += `};\n`;
|
|
796
|
-
} else {
|
|
797
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
798
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
799
|
-
hookCode += ` return useMutation({\n`;
|
|
800
|
-
hookCode += ` mutationFn: async (data: ${requestBodyType}) => {\n`;
|
|
801
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
802
|
-
hookCode += ` return response.data;\n`;
|
|
803
|
-
hookCode += ` },\n`;
|
|
804
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
805
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
806
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
807
|
-
hookCode += ` },\n`;
|
|
808
|
-
hookCode += ` });\n`;
|
|
809
|
-
hookCode += `};\n`;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
return hookCode;
|
|
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
|
+
}
|
|
814
920
|
}
|
|
815
921
|
|
|
816
922
|
|
|
@@ -935,4 +1041,172 @@ export class SwaggerDocGenerator {
|
|
|
935
1041
|
}
|
|
936
1042
|
}
|
|
937
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
|
+
}
|
|
938
1212
|
}
|