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/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
  /**
@@ -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
- const interfaceName = `${toPascalCase(operationId)}Params`;
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
- const hookName = `use${toPascalCase(operationId)}`;
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 && endpointInfo.responses['200']) {
697
- const responseSchema = endpointInfo.responses['200'].content?.['application/json']?.schema;
698
- if (responseSchema) {
699
- 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
+ }
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 pathWithParams = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
716
- const axiosPath = `\`\${process.env.REACT_APP_API_BASE_URL || ''}${pathWithParams}\``;
717
-
718
- // Generate the hook code
719
- let hookCode = '';
720
-
721
- if (method.toLowerCase() === 'get') {
722
- // For GET requests, use useQuery
723
- const hasParams = pathParams.length > 0 || queryParams.length > 0;
724
- if (hasParams) {
725
- const paramInterfaceName = `${toPascalCase(operationId)}Params`;
726
- hookCode += `export const ${hookName} = (params: ${paramInterfaceName}) => {\n`;
727
- hookCode += ` return useQuery({\n`;
728
- hookCode += ` queryKey: ['${operationId}', params],\n`;
729
- hookCode += ` queryFn: async () => {\n`;
730
- hookCode += ` const response = await axios.get<${responseType}>(${axiosPath}, { params });\n`;
731
- hookCode += ` return response.data;\n`;
732
- hookCode += ` },\n`;
733
- hookCode += ` });\n`;
734
- hookCode += `};\n`;
735
- } else {
736
- hookCode += `export const ${hookName} = () => {\n`;
737
- hookCode += ` return useQuery({\n`;
738
- hookCode += ` queryKey: ['${operationId}'],\n`;
739
- hookCode += ` queryFn: async () => {\n`;
740
- hookCode += ` const response = await axios.get<${responseType}>(${axiosPath});\n`;
741
- hookCode += ` return response.data;\n`;
742
- hookCode += ` },\n`;
743
- hookCode += ` });\n`;
744
- hookCode += `};\n`;
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
  }