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/dist/index.js
CHANGED
|
@@ -40,6 +40,8 @@ exports.SwaggerDocGenerator = void 0;
|
|
|
40
40
|
const axios_1 = __importDefault(require("axios"));
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
+
const template_helpers_1 = require("./helpers/template.helpers");
|
|
44
|
+
const type_helpers_1 = require("./helpers/type.helpers");
|
|
43
45
|
/**
|
|
44
46
|
* Transforms a string to PascalCase
|
|
45
47
|
*/
|
|
@@ -60,76 +62,27 @@ function toCamelCase(str) {
|
|
|
60
62
|
})
|
|
61
63
|
.replace(/\s+/g, '');
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
// Handle allOf (used for composition/references)
|
|
75
|
-
if (typeDef.allOf && Array.isArray(typeDef.allOf)) {
|
|
76
|
-
return typeDef.allOf.map((item) => {
|
|
77
|
-
if (item.$ref) {
|
|
78
|
-
return item.$ref.split('/').pop();
|
|
79
|
-
}
|
|
80
|
-
else if (item.type) {
|
|
81
|
-
return convertTypeToTs(item, schemaComponents);
|
|
82
|
-
}
|
|
83
|
-
return 'any';
|
|
84
|
-
}).filter(Boolean).join(' & ') || 'any';
|
|
85
|
-
}
|
|
86
|
-
if (Array.isArray(typeDef.type)) {
|
|
87
|
-
// Handle union types like ["string", "null"]
|
|
88
|
-
if (typeDef.type.includes('null')) {
|
|
89
|
-
const nonNullType = typeDef.type.find((t) => t !== 'null');
|
|
90
|
-
return `${convertTypeToTs({ ...typeDef, type: nonNullType }, schemaComponents)} | null`;
|
|
91
|
-
}
|
|
92
|
-
return 'any';
|
|
65
|
+
class SwaggerDocGenerator {
|
|
66
|
+
/**
|
|
67
|
+
* Transforms a string to PascalCase
|
|
68
|
+
*/
|
|
69
|
+
toPascalCase(str) {
|
|
70
|
+
return str
|
|
71
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
72
|
+
return index === 0 ? word.toUpperCase() : word.toUpperCase();
|
|
73
|
+
})
|
|
74
|
+
.replace(/\s+/g, '');
|
|
93
75
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
case 'integer':
|
|
104
|
-
case 'number':
|
|
105
|
-
return 'number';
|
|
106
|
-
case 'boolean':
|
|
107
|
-
return 'boolean';
|
|
108
|
-
case 'array':
|
|
109
|
-
if (typeDef.items) {
|
|
110
|
-
return `${convertTypeToTs(typeDef.items, schemaComponents)}[]`;
|
|
111
|
-
}
|
|
112
|
-
return 'any[]';
|
|
113
|
-
case 'object':
|
|
114
|
-
if (typeDef.properties) {
|
|
115
|
-
// Inline object definition
|
|
116
|
-
const fields = Object.entries(typeDef.properties)
|
|
117
|
-
.map(([propName, propSchema]) => {
|
|
118
|
-
const required = typeDef.required && typeDef.required.includes(propName);
|
|
119
|
-
const optional = !required ? '?' : '';
|
|
120
|
-
return ` ${propName}${optional}: ${convertTypeToTs(propSchema, schemaComponents)};`;
|
|
121
|
-
})
|
|
122
|
-
.join('\n');
|
|
123
|
-
return `{\n${fields}\n}`;
|
|
124
|
-
}
|
|
125
|
-
return 'Record<string, any>';
|
|
126
|
-
case 'null':
|
|
127
|
-
return 'null';
|
|
128
|
-
default:
|
|
129
|
-
return 'any';
|
|
76
|
+
/**
|
|
77
|
+
* Transforms a string to camelCase
|
|
78
|
+
*/
|
|
79
|
+
toCamelCase(str) {
|
|
80
|
+
return str
|
|
81
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
82
|
+
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
|
83
|
+
})
|
|
84
|
+
.replace(/\s+/g, '');
|
|
130
85
|
}
|
|
131
|
-
}
|
|
132
|
-
class SwaggerDocGenerator {
|
|
133
86
|
/**
|
|
134
87
|
* Fetches the Swagger/OpenAPI JSON from a given URL
|
|
135
88
|
*/
|
|
@@ -242,6 +195,149 @@ class SwaggerDocGenerator {
|
|
|
242
195
|
});
|
|
243
196
|
return typeDefs;
|
|
244
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Generates React hooks from the paths in Swagger doc organized by tag
|
|
200
|
+
*/
|
|
201
|
+
/**
|
|
202
|
+
* Converts OpenAPI types to TypeScript types
|
|
203
|
+
*/
|
|
204
|
+
convertTypeToTs(typeDef, schemaComponents) {
|
|
205
|
+
if (!typeDef)
|
|
206
|
+
return 'any';
|
|
207
|
+
if (typeDef.$ref) {
|
|
208
|
+
// Extract the type name from the reference
|
|
209
|
+
const refTypeName = typeDef.$ref.split('/').pop();
|
|
210
|
+
return refTypeName || 'any';
|
|
211
|
+
}
|
|
212
|
+
// Handle allOf (used for composition/references) - combine all properties
|
|
213
|
+
if (typeDef.allOf && Array.isArray(typeDef.allOf)) {
|
|
214
|
+
const combinedProperties = {};
|
|
215
|
+
const refTypes = [];
|
|
216
|
+
for (const item of typeDef.allOf) {
|
|
217
|
+
if (item.$ref) {
|
|
218
|
+
// Extract the type name from the reference
|
|
219
|
+
const refTypeName = item.$ref.split('/').pop();
|
|
220
|
+
if (refTypeName) {
|
|
221
|
+
refTypes.push(refTypeName);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (item.type === 'object' && item.properties) {
|
|
225
|
+
// Combine properties from inline object definitions
|
|
226
|
+
Object.assign(combinedProperties, item.properties);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (refTypes.length > 0 && Object.keys(combinedProperties).length > 0) {
|
|
230
|
+
// We have both references and inline properties
|
|
231
|
+
const inlineDef = {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: combinedProperties,
|
|
234
|
+
required: typeDef.required
|
|
235
|
+
};
|
|
236
|
+
const inlineType = this.convertTypeToTs(inlineDef, schemaComponents);
|
|
237
|
+
return `${refTypes.join(' & ')} & ${inlineType}`;
|
|
238
|
+
}
|
|
239
|
+
else if (refTypes.length > 0) {
|
|
240
|
+
// Only references
|
|
241
|
+
return refTypes.join(' & ');
|
|
242
|
+
}
|
|
243
|
+
else if (Object.keys(combinedProperties).length > 0) {
|
|
244
|
+
// Only inline properties
|
|
245
|
+
return this.convertTypeToTs({
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: combinedProperties,
|
|
248
|
+
required: typeDef.required
|
|
249
|
+
}, schemaComponents);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
return 'any';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Handle oneOf (union types)
|
|
256
|
+
if (typeDef.oneOf && Array.isArray(typeDef.oneOf)) {
|
|
257
|
+
return typeDef.oneOf.map((item) => {
|
|
258
|
+
if (item.$ref) {
|
|
259
|
+
return item.$ref.split('/').pop();
|
|
260
|
+
}
|
|
261
|
+
else if (item.type) {
|
|
262
|
+
return this.convertTypeToTs(item, schemaComponents);
|
|
263
|
+
}
|
|
264
|
+
return 'any';
|
|
265
|
+
}).filter(Boolean).join(' | ') || 'any';
|
|
266
|
+
}
|
|
267
|
+
// Handle anyOf (union types)
|
|
268
|
+
if (typeDef.anyOf && Array.isArray(typeDef.anyOf)) {
|
|
269
|
+
return typeDef.anyOf.map((item) => {
|
|
270
|
+
if (item.$ref) {
|
|
271
|
+
return item.$ref.split('/').pop();
|
|
272
|
+
}
|
|
273
|
+
else if (item.type) {
|
|
274
|
+
return this.convertTypeToTs(item, schemaComponents);
|
|
275
|
+
}
|
|
276
|
+
return 'any';
|
|
277
|
+
}).filter(Boolean).join(' | ') || 'any';
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(typeDef.type)) {
|
|
280
|
+
// Handle union types like ["string", "null"]
|
|
281
|
+
if (typeDef.type.includes('null')) {
|
|
282
|
+
const nonNullTypes = typeDef.type.filter((t) => t !== 'null');
|
|
283
|
+
if (nonNullTypes.length === 1) {
|
|
284
|
+
return `${this.convertTypeToTs({ ...typeDef, type: nonNullTypes[0] }, schemaComponents)} | null`;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Handle complex union types with null
|
|
288
|
+
const nonNullTypeStr = nonNullTypes
|
|
289
|
+
.map((t) => this.convertTypeToTs({ ...typeDef, type: t }, schemaComponents))
|
|
290
|
+
.join(' | ');
|
|
291
|
+
return `${nonNullTypeStr} | null`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Handle other array type unions
|
|
295
|
+
return typeDef.type
|
|
296
|
+
.map((t) => this.convertTypeToTs({ ...typeDef, type: t }, schemaComponents))
|
|
297
|
+
.join(' | ') || 'any';
|
|
298
|
+
}
|
|
299
|
+
switch (typeDef.type) {
|
|
300
|
+
case 'string':
|
|
301
|
+
if (typeDef.enum) {
|
|
302
|
+
return `"${typeDef.enum.join('" | "')}"`;
|
|
303
|
+
}
|
|
304
|
+
if (typeDef.format === 'date' || typeDef.format === 'date-time') {
|
|
305
|
+
return 'string';
|
|
306
|
+
}
|
|
307
|
+
return 'string';
|
|
308
|
+
case 'integer':
|
|
309
|
+
case 'number':
|
|
310
|
+
return 'number';
|
|
311
|
+
case 'boolean':
|
|
312
|
+
return 'boolean';
|
|
313
|
+
case 'array':
|
|
314
|
+
if (typeDef.items) {
|
|
315
|
+
return `${this.convertTypeToTs(typeDef.items, schemaComponents)}[]`;
|
|
316
|
+
}
|
|
317
|
+
return 'any[]';
|
|
318
|
+
case 'object':
|
|
319
|
+
if (typeDef.properties) {
|
|
320
|
+
// Inline object definition
|
|
321
|
+
const fields = Object.entries(typeDef.properties)
|
|
322
|
+
.map(([propName, propSchema]) => {
|
|
323
|
+
const required = typeDef.required && typeDef.required.includes(propName);
|
|
324
|
+
const optional = !required ? '?' : '';
|
|
325
|
+
const type = this.convertTypeToTs(propSchema, schemaComponents);
|
|
326
|
+
// Get the description for JSDoc if available
|
|
327
|
+
const propDescription = propSchema.description || propSchema.title;
|
|
328
|
+
const jsDoc = propDescription ? ` /** ${propDescription} */\n` : '';
|
|
329
|
+
return `${jsDoc} ${propName}${optional}: ${type};`;
|
|
330
|
+
})
|
|
331
|
+
.join('\n');
|
|
332
|
+
return `{\n${fields}\n }`;
|
|
333
|
+
}
|
|
334
|
+
return 'Record<string, any>';
|
|
335
|
+
case 'null':
|
|
336
|
+
return 'null';
|
|
337
|
+
default:
|
|
338
|
+
return 'any';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
245
341
|
/**
|
|
246
342
|
* Generates a single TypeScript type definition
|
|
247
343
|
*/
|
|
@@ -250,15 +346,15 @@ class SwaggerDocGenerator {
|
|
|
250
346
|
// Enum type
|
|
251
347
|
return `export type ${typeName} = ${schema.enum.map((val) => `'${val}'`).join(' | ')};\n`;
|
|
252
348
|
}
|
|
253
|
-
if (schema.oneOf || schema.anyOf
|
|
254
|
-
// Union type or complex type
|
|
255
|
-
const typeOption = schema.oneOf ? 'oneOf' :
|
|
349
|
+
if (schema.oneOf || schema.anyOf) {
|
|
350
|
+
// Union type or complex type (oneOf/anyOf)
|
|
351
|
+
const typeOption = schema.oneOf ? 'oneOf' : 'anyOf';
|
|
256
352
|
const types = schema[typeOption].map((item) => {
|
|
257
353
|
if (item.$ref) {
|
|
258
354
|
return item.$ref.split('/').pop();
|
|
259
355
|
}
|
|
260
356
|
else if (item.type) {
|
|
261
|
-
return convertTypeToTs(item, allSchemas);
|
|
357
|
+
return this.convertTypeToTs(item, allSchemas);
|
|
262
358
|
}
|
|
263
359
|
else {
|
|
264
360
|
return 'any';
|
|
@@ -266,6 +362,24 @@ class SwaggerDocGenerator {
|
|
|
266
362
|
}).filter(Boolean);
|
|
267
363
|
return `export type ${typeName} = ${types.join(' | ')};\n`;
|
|
268
364
|
}
|
|
365
|
+
if (schema.allOf) {
|
|
366
|
+
// Handle allOf - composition of multiple schemas
|
|
367
|
+
const allParts = [];
|
|
368
|
+
for (const part of schema.allOf) {
|
|
369
|
+
if (part.$ref) {
|
|
370
|
+
const refTypeName = part.$ref.split('/').pop();
|
|
371
|
+
if (refTypeName) {
|
|
372
|
+
allParts.push(refTypeName);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (part.type === 'object' && part.properties) {
|
|
376
|
+
// Create a temporary interface for inline object
|
|
377
|
+
const inlineInterface = this.generateInlineObjectInterface(part, `${typeName}Inline`, allSchemas);
|
|
378
|
+
allParts.push(inlineInterface);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return `export type ${typeName} = ${allParts.join(' & ')};\n`;
|
|
382
|
+
}
|
|
269
383
|
if (schema.type === 'object') {
|
|
270
384
|
// Object type
|
|
271
385
|
let result = `export interface ${typeName} {\n`;
|
|
@@ -273,18 +387,39 @@ class SwaggerDocGenerator {
|
|
|
273
387
|
Object.entries(schema.properties).forEach(([propName, propSchema]) => {
|
|
274
388
|
const required = schema.required && schema.required.includes(propName);
|
|
275
389
|
const optional = !required ? '?' : '';
|
|
276
|
-
// Add JSDoc comment if available
|
|
277
|
-
|
|
278
|
-
|
|
390
|
+
// Add JSDoc comment if available (using title or description)
|
|
391
|
+
const jsDoc = propSchema.title || propSchema.description;
|
|
392
|
+
if (jsDoc) {
|
|
393
|
+
result += ` /** ${jsDoc} */\n`;
|
|
279
394
|
}
|
|
280
|
-
result += ` ${propName}${optional}: ${convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
395
|
+
result += ` ${propName}${optional}: ${this.convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
281
396
|
});
|
|
282
397
|
}
|
|
283
398
|
result += '}\n';
|
|
284
399
|
return result;
|
|
285
400
|
}
|
|
286
401
|
// For other types (string, number, etc.) that might have additional properties
|
|
287
|
-
return `export type ${typeName} = ${convertTypeToTs(schema, allSchemas)};\n`;
|
|
402
|
+
return `export type ${typeName} = ${this.convertTypeToTs(schema, allSchemas)};\n`;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Generates an inline object interface for allOf composition
|
|
406
|
+
*/
|
|
407
|
+
generateInlineObjectInterface(schema, tempName, allSchemas) {
|
|
408
|
+
if (!schema.properties)
|
|
409
|
+
return 'any';
|
|
410
|
+
let result = '{\n';
|
|
411
|
+
Object.entries(schema.properties).forEach(([propName, propSchema]) => {
|
|
412
|
+
const required = schema.required && schema.required.includes(propName);
|
|
413
|
+
const optional = !required ? '?' : '';
|
|
414
|
+
// Add JSDoc comment if available (using title or description)
|
|
415
|
+
const jsDoc = propSchema.title || propSchema.description;
|
|
416
|
+
if (jsDoc) {
|
|
417
|
+
result += ` /** ${jsDoc} */\n`;
|
|
418
|
+
}
|
|
419
|
+
result += ` ${propName}${optional}: ${this.convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
420
|
+
});
|
|
421
|
+
result += ' }';
|
|
422
|
+
return result;
|
|
288
423
|
}
|
|
289
424
|
/**
|
|
290
425
|
* Generates React hooks from the paths in Swagger doc organized by tag
|
|
@@ -577,14 +712,14 @@ class SwaggerDocGenerator {
|
|
|
577
712
|
// Add path parameters
|
|
578
713
|
pathParams.forEach((param) => {
|
|
579
714
|
const required = param.required ? '' : '?';
|
|
580
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
581
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
715
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
716
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
582
717
|
});
|
|
583
718
|
// Add query parameters
|
|
584
719
|
queryParams.forEach((param) => {
|
|
585
720
|
const required = param.required ? '' : '?';
|
|
586
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
587
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
721
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
722
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
588
723
|
});
|
|
589
724
|
paramsInterface += '}\n';
|
|
590
725
|
return paramsInterface;
|
|
@@ -596,29 +731,36 @@ class SwaggerDocGenerator {
|
|
|
596
731
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
597
732
|
// Extract action name from operationId to create cleaner hook names
|
|
598
733
|
// e.g. configController_updateConfig -> useUpdateConfig instead of useConfigController_updateConfig
|
|
599
|
-
let hookName = `use${toPascalCase(operationId)}`;
|
|
734
|
+
let hookName = `use${this.toPascalCase(operationId)}`;
|
|
600
735
|
// Check if operationId follows pattern controller_action and simplify to action
|
|
601
736
|
if (operationId.includes('_')) {
|
|
602
737
|
const parts = operationId.split('_');
|
|
603
738
|
if (parts.length >= 2) {
|
|
604
739
|
// Use just the action part as the hook name
|
|
605
|
-
hookName = `use${toPascalCase(parts[parts.length - 1])}`;
|
|
740
|
+
hookName = `use${this.toPascalCase(parts[parts.length - 1])}`;
|
|
606
741
|
}
|
|
607
742
|
}
|
|
608
743
|
else {
|
|
609
744
|
// For operationIds without underscores, keep the original naming
|
|
610
|
-
hookName = `use${toPascalCase(operationId)}`;
|
|
745
|
+
hookName = `use${this.toPascalCase(operationId)}`;
|
|
611
746
|
}
|
|
612
747
|
const hookType = method.toLowerCase() === 'get' ? 'useQuery' : 'useMutation';
|
|
613
748
|
// Use unique parameter interface name
|
|
614
749
|
const pathParams = endpointInfo.parameters?.filter((p) => p.in === 'path') || [];
|
|
615
750
|
const queryParams = endpointInfo.parameters?.filter((p) => p.in === 'query') || [];
|
|
616
|
-
// Determine response type
|
|
751
|
+
// Determine response type by checking common success response codes
|
|
617
752
|
let responseType = 'any';
|
|
618
|
-
if (endpointInfo.responses
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
753
|
+
if (endpointInfo.responses) {
|
|
754
|
+
// Check for success responses in order of preference: 200, 201, 204, etc.
|
|
755
|
+
const successCodes = ['200', '201', '204', '202', '203', '205'];
|
|
756
|
+
for (const code of successCodes) {
|
|
757
|
+
if (endpointInfo.responses[code]) {
|
|
758
|
+
const responseSchema = endpointInfo.responses[code].content?.['application/json']?.schema;
|
|
759
|
+
if (responseSchema) {
|
|
760
|
+
responseType = this.convertTypeToTs(responseSchema, schemas);
|
|
761
|
+
break; // Use the first success response found
|
|
762
|
+
}
|
|
763
|
+
}
|
|
622
764
|
}
|
|
623
765
|
}
|
|
624
766
|
// Generate request body parameter if needed
|
|
@@ -627,80 +769,39 @@ class SwaggerDocGenerator {
|
|
|
627
769
|
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
628
770
|
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
629
771
|
if (bodySchema) {
|
|
630
|
-
requestBodyType = convertTypeToTs(bodySchema, schemas);
|
|
772
|
+
requestBodyType = this.convertTypeToTs(bodySchema, schemas);
|
|
631
773
|
hasBody = true;
|
|
632
774
|
}
|
|
633
775
|
}
|
|
634
776
|
// Format the path for use in the code (handle path parameters) - without base URL
|
|
635
|
-
const formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
hookCode += ` },\n`;
|
|
663
|
-
hookCode += ` });\n`;
|
|
664
|
-
hookCode += `};\n`;
|
|
665
|
-
}
|
|
777
|
+
const formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${this.toCamelCase(param)}}`);
|
|
778
|
+
// Prepare data for the template
|
|
779
|
+
const hookData = {
|
|
780
|
+
hookName: hookName,
|
|
781
|
+
operationId: operationId,
|
|
782
|
+
method: method.toLowerCase(),
|
|
783
|
+
responseType: responseType,
|
|
784
|
+
requestBodyType: requestBodyType,
|
|
785
|
+
hasParams: pathParams.length > 0 || queryParams.length > 0,
|
|
786
|
+
hasPathParams: pathParams.length > 0,
|
|
787
|
+
paramInterfaceName: `${hookName.replace('use', '')}Params`,
|
|
788
|
+
formattedPath: formattedPath,
|
|
789
|
+
isGetRequest: method.toLowerCase() === 'get'
|
|
790
|
+
};
|
|
791
|
+
// Load and compile the individual hook template
|
|
792
|
+
const fs = require('fs');
|
|
793
|
+
const pathModule = require('path');
|
|
794
|
+
const templatePath = pathModule.join(__dirname, '..', 'templates', 'hooks', 'individual-hook.hbs');
|
|
795
|
+
try {
|
|
796
|
+
const templateSource = fs.readFileSync(templatePath, 'utf8');
|
|
797
|
+
const Handlebars = require('handlebars');
|
|
798
|
+
const template = Handlebars.compile(templateSource);
|
|
799
|
+
return template(hookData);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
console.error(`Error reading template file: ${error.message}`);
|
|
803
|
+
return `// Error generating hook for ${operationId}: ${error.message}`;
|
|
666
804
|
}
|
|
667
|
-
else {
|
|
668
|
-
// For non-GET requests, use useMutation
|
|
669
|
-
const hasPathParams = pathParams.length > 0;
|
|
670
|
-
if (hasPathParams) {
|
|
671
|
-
// Generate simpler parameter interface name based on hook name instead of operationId
|
|
672
|
-
const paramInterfaceName = `${hookName.replace('use', '')}Params`;
|
|
673
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
674
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
675
|
-
hookCode += ` return useMutation({\n`;
|
|
676
|
-
hookCode += ` mutationFn: async ({ params, data }: { params: ${paramInterfaceName}; data: ${requestBodyType} }) => {\n`;
|
|
677
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
678
|
-
hookCode += ` return response.data;\n`;
|
|
679
|
-
hookCode += ` },\n`;
|
|
680
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
681
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
682
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
683
|
-
hookCode += ` },\n`;
|
|
684
|
-
hookCode += ` });\n`;
|
|
685
|
-
hookCode += `};\n`;
|
|
686
|
-
}
|
|
687
|
-
else {
|
|
688
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
689
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
690
|
-
hookCode += ` return useMutation({\n`;
|
|
691
|
-
hookCode += ` mutationFn: async (data: ${requestBodyType}) => {\n`;
|
|
692
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
693
|
-
hookCode += ` return response.data;\n`;
|
|
694
|
-
hookCode += ` },\n`;
|
|
695
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
696
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
697
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
698
|
-
hookCode += ` },\n`;
|
|
699
|
-
hookCode += ` });\n`;
|
|
700
|
-
hookCode += `};\n`;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
return hookCode;
|
|
704
805
|
}
|
|
705
806
|
/**
|
|
706
807
|
* Generate operation ID from path and method if not provided
|
|
@@ -808,6 +909,150 @@ class SwaggerDocGenerator {
|
|
|
808
909
|
}
|
|
809
910
|
}
|
|
810
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Generates frontend resources using Handlebars templates
|
|
914
|
+
*/
|
|
915
|
+
generateHandlebarsResources(swaggerDoc, templatePaths = {}) {
|
|
916
|
+
const resourcesByTag = new Map();
|
|
917
|
+
const schemas = swaggerDoc.components?.schemas || {};
|
|
918
|
+
// Group endpoints by tag
|
|
919
|
+
const endpointsByTag = {};
|
|
920
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
921
|
+
Object.entries(methods).forEach(([method, endpointInfo]) => {
|
|
922
|
+
// Determine the tag for this endpoint
|
|
923
|
+
const tag = (endpointInfo.tags && endpointInfo.tags[0]) ? endpointInfo.tags[0] : 'General';
|
|
924
|
+
if (!endpointsByTag[tag]) {
|
|
925
|
+
endpointsByTag[tag] = [];
|
|
926
|
+
}
|
|
927
|
+
endpointsByTag[tag].push({ path, method, endpointInfo });
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
// Generate resources for each tag
|
|
931
|
+
Object.entries(endpointsByTag).forEach(([tag, endpoints]) => {
|
|
932
|
+
// Prepare context for templates
|
|
933
|
+
const context = {
|
|
934
|
+
title: swaggerDoc.info.title,
|
|
935
|
+
description: swaggerDoc.info.description || swaggerDoc.info.title,
|
|
936
|
+
version: swaggerDoc.info.version,
|
|
937
|
+
tag: tag,
|
|
938
|
+
endpoints: endpoints.map(e => ({
|
|
939
|
+
path: e.path,
|
|
940
|
+
method: e.method.toUpperCase(),
|
|
941
|
+
operationId: e.endpointInfo.operationId || this.generateOperationId(e.path, e.method),
|
|
942
|
+
summary: e.endpointInfo.summary,
|
|
943
|
+
description: e.endpointInfo.description,
|
|
944
|
+
parameters: e.endpointInfo.parameters || [],
|
|
945
|
+
responses: e.endpointInfo.responses,
|
|
946
|
+
requestBody: e.endpointInfo.requestBody
|
|
947
|
+
})),
|
|
948
|
+
schemas: schemas,
|
|
949
|
+
hasImportTypes: false,
|
|
950
|
+
usedTypeNames: [],
|
|
951
|
+
paramInterfaces: [],
|
|
952
|
+
hooks: [],
|
|
953
|
+
typeDefinitions: []
|
|
954
|
+
};
|
|
955
|
+
// Find types used in this tag
|
|
956
|
+
const directlyUsedSchemas = new Set();
|
|
957
|
+
if (schemas) {
|
|
958
|
+
Object.entries(schemas).forEach(([typeName, schema]) => {
|
|
959
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
960
|
+
directlyUsedSchemas.add(typeName);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const allNeededSchemas = this.findAllReferencedSchemas(directlyUsedSchemas, schemas);
|
|
965
|
+
// Generate TypeScript types
|
|
966
|
+
let typesContent = '';
|
|
967
|
+
if (schemas) {
|
|
968
|
+
for (const typeName of allNeededSchemas) {
|
|
969
|
+
const schema = schemas[typeName];
|
|
970
|
+
if (schema) {
|
|
971
|
+
const typeDef = (0, type_helpers_1.generateSingleTypeDefinition)(typeName, schema, schemas);
|
|
972
|
+
typesContent += typeDef + '\n';
|
|
973
|
+
context.typeDefinitions.push(typeDef);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Check if there are used types for import
|
|
978
|
+
if (allNeededSchemas.size > 0) {
|
|
979
|
+
context.hasImportTypes = true;
|
|
980
|
+
context.usedTypeNames = Array.from(allNeededSchemas);
|
|
981
|
+
}
|
|
982
|
+
// Generate parameter interfaces
|
|
983
|
+
const allParamInterfaces = [];
|
|
984
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
985
|
+
const paramInterface = this.generateParamInterface(path, method, endpointInfo, schemas);
|
|
986
|
+
if (paramInterface && !allParamInterfaces.includes(paramInterface)) {
|
|
987
|
+
allParamInterfaces.push(paramInterface);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
context.paramInterfaces = allParamInterfaces;
|
|
991
|
+
// Generate individual hooks
|
|
992
|
+
const allHooks = [];
|
|
993
|
+
const endpointHookContents = [];
|
|
994
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
995
|
+
const hookContent = this.generateReactQueryHook(path, method, endpointInfo, schemas);
|
|
996
|
+
allHooks.push(hookContent);
|
|
997
|
+
endpointHookContents.push(hookContent); // Store for template context
|
|
998
|
+
});
|
|
999
|
+
context.hooks = allHooks;
|
|
1000
|
+
context.endpointHooks = endpointHookContents;
|
|
1001
|
+
// Generate resources using specified templates
|
|
1002
|
+
let hooksContent = '';
|
|
1003
|
+
if (templatePaths.hooks) {
|
|
1004
|
+
try {
|
|
1005
|
+
// Add utility functions to context for use in templates
|
|
1006
|
+
context['camelCase'] = (str) => this.toCamelCase(str);
|
|
1007
|
+
context['pascalCase'] = (str) => this.toPascalCase(str);
|
|
1008
|
+
hooksContent = (0, template_helpers_1.compileTemplate)(templatePaths.hooks, context);
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
// If template doesn't exist or fails, fall back to default generation
|
|
1012
|
+
console.warn(`Failed to compile hooks template: ${templatePaths.hooks}`, error);
|
|
1013
|
+
// Use the existing method as fallback
|
|
1014
|
+
hooksContent = `// ${this.toPascalCase(tag)} API Hooks\n`;
|
|
1015
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
1016
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
1017
|
+
if (context.hasImportTypes) {
|
|
1018
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${this.toCamelCase(tag)}.types';\n\n`;
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
hooksContent += `\n`;
|
|
1022
|
+
}
|
|
1023
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
1024
|
+
hooksContent += interfaceCode + '\n';
|
|
1025
|
+
});
|
|
1026
|
+
allHooks.forEach(hookCode => {
|
|
1027
|
+
hooksContent += hookCode + '\n';
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
// Default generation if no template is provided
|
|
1033
|
+
hooksContent = `// ${this.toPascalCase(tag)} API Hooks\n`;
|
|
1034
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
1035
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
1036
|
+
if (context.hasImportTypes) {
|
|
1037
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${this.toCamelCase(tag)}.types';\n\n`;
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
hooksContent += `\n`;
|
|
1041
|
+
}
|
|
1042
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
1043
|
+
hooksContent += interfaceCode + '\n';
|
|
1044
|
+
});
|
|
1045
|
+
allHooks.forEach(hookCode => {
|
|
1046
|
+
hooksContent += hookCode + '\n';
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
resourcesByTag.set(tag, {
|
|
1050
|
+
hooks: hooksContent,
|
|
1051
|
+
types: typesContent
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
return resourcesByTag;
|
|
1055
|
+
}
|
|
811
1056
|
}
|
|
812
1057
|
exports.SwaggerDocGenerator = SwaggerDocGenerator;
|
|
813
1058
|
//# sourceMappingURL=index.js.map
|