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/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
- * Converts OpenAPI types to TypeScript types
102
- */
103
- function convertTypeToTs(typeDef: any, schemaComponents: { [key: string]: any }): string {
104
- if (!typeDef) return 'any';
105
-
106
- if (typeDef.$ref) {
107
- // Extract the type name from the reference
108
- const refTypeName = typeDef.$ref.split('/').pop();
109
- return refTypeName || 'any';
110
- }
111
-
112
- // Handle allOf (used for composition/references)
113
- if (typeDef.allOf && Array.isArray(typeDef.allOf)) {
114
- return typeDef.allOf.map((item: any) => {
115
- if (item.$ref) {
116
- return item.$ref.split('/').pop();
117
- } else if (item.type) {
118
- return convertTypeToTs(item, schemaComponents);
119
- }
120
- return 'any';
121
- }).filter(Boolean).join(' & ') || 'any';
122
- }
123
-
124
- if (Array.isArray(typeDef.type)) {
125
- // Handle union types like ["string", "null"]
126
- if (typeDef.type.includes('null')) {
127
- const nonNullType = typeDef.type.find((t: string) => t !== 'null');
128
- return `${convertTypeToTs({...typeDef, type: nonNullType}, schemaComponents)} | null`;
129
- }
130
- return 'any';
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
- switch (typeDef.type) {
134
- case 'string':
135
- if (typeDef.enum) {
136
- return `"${typeDef.enum.join('" | "')}"`;
137
- }
138
- if (typeDef.format === 'date' || typeDef.format === 'date-time') {
139
- return 'string';
140
- }
141
- return 'string';
142
- case 'integer':
143
- case 'number':
144
- return 'number';
145
- case 'boolean':
146
- return 'boolean';
147
- case 'array':
148
- if (typeDef.items) {
149
- return `${convertTypeToTs(typeDef.items, schemaComponents)}[]`;
150
- }
151
- return 'any[]';
152
- case 'object':
153
- if (typeDef.properties) {
154
- // Inline object definition
155
- const fields = Object.entries(typeDef.properties)
156
- .map(([propName, propSchema]: [string, any]) => {
157
- const required = typeDef.required && typeDef.required.includes(propName);
158
- const optional = !required ? '?' : '';
159
- return ` ${propName}${optional}: ${convertTypeToTs(propSchema, schemaComponents)};`;
160
- })
161
- .join('\n');
162
- return `{\n${fields}\n}`;
163
- }
164
- return 'Record<string, any>';
165
- case 'null':
166
- return 'null';
167
- default:
168
- return 'any';
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 || schema.allOf) {
307
- // Union type or complex type
308
- const typeOption = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'allOf';
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
- if (propSchema.description) {
332
- result += ` /** ${propSchema.description} */\n`;
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 && endpointInfo.responses['200']) {
727
- const responseSchema = endpointInfo.responses['200'].content?.['application/json']?.schema;
728
- if (responseSchema) {
729
- responseType = convertTypeToTs(responseSchema, schemas);
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
- const axiosPath = `\`${formattedPath}\``;
747
-
748
- // Generate the hook code
749
- let hookCode = '';
750
-
751
- if (method.toLowerCase() === 'get') {
752
- // For GET requests, use useQuery
753
- const hasParams = pathParams.length > 0 || queryParams.length > 0;
754
- if (hasParams) {
755
- // Generate simpler parameter interface name based on hook name instead of operationId
756
- const paramInterfaceName = `${hookName.replace('use', '')}Params`;
757
- hookCode += `export const ${hookName} = (params: ${paramInterfaceName}) => {\n`;
758
- hookCode += ` return useQuery({\n`;
759
- hookCode += ` queryKey: ['${operationId}', params],\n`;
760
- hookCode += ` queryFn: async () => {\n`;
761
- hookCode += ` const response = await axios.get<${responseType}>(${axiosPath}, { params });\n`;
762
- hookCode += ` return response.data;\n`;
763
- hookCode += ` },\n`;
764
- hookCode += ` });\n`;
765
- hookCode += `};\n`;
766
- } else {
767
- hookCode += `export const ${hookName} = () => {\n`;
768
- hookCode += ` return useQuery({\n`;
769
- hookCode += ` queryKey: ['${operationId}'],\n`;
770
- hookCode += ` queryFn: async () => {\n`;
771
- hookCode += ` const response = await axios.get<${responseType}>(${axiosPath});\n`;
772
- hookCode += ` return response.data;\n`;
773
- hookCode += ` },\n`;
774
- hookCode += ` });\n`;
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
  }