@0kmpo/openapi-clean-arch-generator 1.3.10

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.
Files changed (52) hide show
  1. package/.gitea/workflows/lint.yaml +41 -0
  2. package/.gitea/workflows/publish.yml +105 -0
  3. package/.openapi-generator-ignore +33 -0
  4. package/.prettierrc +7 -0
  5. package/LICENSE +21 -0
  6. package/README.md +333 -0
  7. package/dist/main.js +180 -0
  8. package/eslint.config.js +33 -0
  9. package/example-swagger.yaml +150 -0
  10. package/generation-config.json +24 -0
  11. package/main.ts +233 -0
  12. package/openapitools.json +23 -0
  13. package/package.json +70 -0
  14. package/src/generators/clean-arch.generator.ts +537 -0
  15. package/src/generators/dto.generator.ts +126 -0
  16. package/src/generators/lint.generator.ts +124 -0
  17. package/src/generators/report.generator.ts +80 -0
  18. package/src/swagger/analyzer.ts +32 -0
  19. package/src/types/cli.types.ts +36 -0
  20. package/src/types/generation.types.ts +50 -0
  21. package/src/types/index.ts +8 -0
  22. package/src/types/openapi.types.ts +126 -0
  23. package/src/types/swagger.types.ts +9 -0
  24. package/src/utils/config.ts +118 -0
  25. package/src/utils/environment-finder.ts +53 -0
  26. package/src/utils/filesystem.ts +31 -0
  27. package/src/utils/logger.ts +60 -0
  28. package/src/utils/mock-value-resolver.ts +70 -0
  29. package/src/utils/name-formatter.ts +12 -0
  30. package/src/utils/openapi-generator.ts +24 -0
  31. package/src/utils/prompt.ts +183 -0
  32. package/src/utils/type-mapper.ts +14 -0
  33. package/templates/api.repository.contract.mustache +34 -0
  34. package/templates/api.repository.impl.mock.mustache +21 -0
  35. package/templates/api.repository.impl.mustache +58 -0
  36. package/templates/api.repository.impl.spec.mustache +97 -0
  37. package/templates/api.use-cases.contract.mustache +34 -0
  38. package/templates/api.use-cases.impl.mustache +32 -0
  39. package/templates/api.use-cases.impl.spec.mustache +94 -0
  40. package/templates/api.use-cases.mock.mustache +21 -0
  41. package/templates/dto.mock.mustache +16 -0
  42. package/templates/mapper.mustache +28 -0
  43. package/templates/mapper.spec.mustache +39 -0
  44. package/templates/model-entity.mustache +24 -0
  45. package/templates/model-entity.spec.mustache +34 -0
  46. package/templates/model.mock.mustache +14 -0
  47. package/templates/model.mustache +20 -0
  48. package/templates/repository.provider.mock.mustache +20 -0
  49. package/templates/repository.provider.mustache +26 -0
  50. package/templates/use-cases.provider.mock.mustache +20 -0
  51. package/templates/use-cases.provider.mustache +26 -0
  52. package/tsconfig.json +17 -0
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@0kmpo/openapi-clean-arch-generator",
3
+ "version": "1.3.10",
4
+ "description": "Angular Clean Architecture generator from OpenAPI/Swagger",
5
+ "main": "dist/main.js",
6
+ "bin": {
7
+ "generate-clean-arch": "./dist/main.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && cp -r templates dist/",
11
+ "postbuild": "bun -e \"const fs=require('fs'); const f='dist/main.js'; const c=fs.readFileSync(f,'utf8'); if(!c.startsWith('#!/usr/bin/env node')) fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c); fs.chmodSync(f, '755');\"",
12
+ "prepublishOnly": "bun run build",
13
+ "generate": "bun dist/main.js",
14
+ "generate:dev": "bun main.ts",
15
+ "binaries": "bun run binary:mac-arm64 && bun run binary:mac-x64 && bun run binary:linux-x64 && bun run binary:linux-arm64 && bun run binary:windows",
16
+ "binary:mac-arm64": "bun build --compile --target=bun-darwin-arm64 --outfile dist/bin/generate-clean-arch-macos-arm64 main.ts",
17
+ "binary:mac-x64": "bun build --compile --target=bun-darwin-x64 --outfile dist/bin/generate-clean-arch-macos-x64 main.ts",
18
+ "binary:linux-x64": "bun build --compile --target=bun-linux-x64 --outfile dist/bin/generate-clean-arch-linux-x64 main.ts",
19
+ "binary:linux-arm64": "bun build --compile --target=bun-linux-arm64 --outfile dist/bin/generate-clean-arch-linux-arm64 main.ts",
20
+ "binary:windows": "bun build --compile --target=bun-windows-x64 --outfile dist/bin/generate-clean-arch-windows-x64.exe main.ts",
21
+ "lint": "bunx --bun eslint 'main.ts' 'src/**/*.ts' -f unix",
22
+ "lint:fix": "bunx --bun eslint 'main.ts' 'src/**/*.ts' --fix -f unix",
23
+ "format": "prettier --write .",
24
+ "setup": "bun add -g @openapitools/openapi-generator-cli"
25
+ },
26
+ "keywords": [
27
+ "openapi",
28
+ "swagger",
29
+ "angular",
30
+ "clean-architecture",
31
+ "code-generator"
32
+ ],
33
+ "author": "Blas Santomé Ocampo",
34
+ "contributors": [
35
+ {
36
+ "name": "Diego Davila Freitas",
37
+ "email": "diego.davilafreitas@gmail.com",
38
+ "url": "https://www.linkedin.com/in/diegodavilafreitas"
39
+ }
40
+ ],
41
+ "license": "MIT",
42
+ "dependencies": {
43
+ "chalk": "^4.1.2",
44
+ "commander": "^11.1.0",
45
+ "fs-extra": "^11.2.0",
46
+ "js-yaml": "^4.1.0",
47
+ "mustache": "^4.2.0",
48
+ "prompts": "^2.4.2"
49
+ },
50
+ "engines": {
51
+ "bun": ">=1.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@eslint/js": "^10.0.1",
55
+ "@types/fs-extra": "^11.0.4",
56
+ "@types/js-yaml": "^4.0.9",
57
+ "@types/mustache": "^4.2.6",
58
+ "@types/node": "^25.5.0",
59
+ "@types/prompts": "^2.4.9",
60
+ "@typescript-eslint/eslint-plugin": "^8.57.1",
61
+ "@typescript-eslint/parser": "^8.57.1",
62
+ "eslint": "^10.1.0",
63
+ "eslint-config-prettier": "^10.1.8",
64
+ "eslint-formatter-unix": "^9.0.1",
65
+ "eslint-plugin-prettier": "^5.5.5",
66
+ "prettier": "^3.8.1",
67
+ "typescript": "^5.9.3",
68
+ "typescript-eslint": "^8.57.1"
69
+ }
70
+ }
@@ -0,0 +1,537 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import mustache from 'mustache';
4
+ import { logStep, logSuccess, logDetail } from '../utils/logger';
5
+ import { mapSwaggerTypeToTs } from '../utils/type-mapper';
6
+ import { toCamelCase } from '../utils/name-formatter';
7
+ import { resolveMockValue } from '../utils/mock-value-resolver';
8
+ import type {
9
+ SwaggerAnalysis,
10
+ OpenApiSchema,
11
+ OpenApiOperation,
12
+ TagOperation,
13
+ TagSummary,
14
+ SelectionFilter,
15
+ GeneratedCount
16
+ } from '../types';
17
+
18
+ /**
19
+ * Extracts the unique tags (in order of appearance) from a SwaggerAnalysis.
20
+ * Only endpoints that have at least one tag are considered; the first tag is used.
21
+ */
22
+ export function extractTagsFromAnalysis(analysis: SwaggerAnalysis): string[] {
23
+ const seen = new Set<string>();
24
+ const tags: string[] = [];
25
+ Object.values(analysis.paths).forEach((pathObj) => {
26
+ Object.values(pathObj as Record<string, unknown>).forEach((opRaw) => {
27
+ const op = opRaw as OpenApiOperation;
28
+ if (op.tags && op.tags.length > 0) {
29
+ const tag = op.tags[0];
30
+ if (!seen.has(tag)) {
31
+ seen.add(tag);
32
+ tags.push(tag);
33
+ }
34
+ }
35
+ });
36
+ });
37
+ return tags;
38
+ }
39
+
40
+ /**
41
+ * Extracts all tags with their operations summary for the interactive selection screen.
42
+ */
43
+ export function extractTagsWithOperations(analysis: SwaggerAnalysis): TagSummary[] {
44
+ const map = new Map<string, TagSummary>();
45
+ Object.entries(analysis.paths).forEach(([pathKey, pathObj]) => {
46
+ Object.entries(pathObj as Record<string, unknown>).forEach(([method, opRaw]) => {
47
+ const op = opRaw as OpenApiOperation;
48
+ if (!op.tags?.length) return;
49
+ const tag = op.tags[0];
50
+ if (!map.has(tag)) map.set(tag, { tag, operations: [] });
51
+ map.get(tag)!.operations.push({
52
+ nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
53
+ method: method.toUpperCase(),
54
+ path: pathKey,
55
+ summary: op.summary || ''
56
+ });
57
+ });
58
+ });
59
+ return [...map.values()];
60
+ }
61
+
62
+ /** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
63
+ export function generateCleanArchitecture(
64
+ analysis: SwaggerAnalysis,
65
+ outputDir: string,
66
+ templatesDir: string,
67
+ tagApiKeyMap: Record<string, string> = {},
68
+ selectionFilter: SelectionFilter = {}
69
+ ): GeneratedCount {
70
+ logStep('Generating Clean Architecture artefacts using Mustache...');
71
+ const generatedCount: GeneratedCount = {
72
+ models: 0,
73
+ repositories: 0,
74
+ mappers: 0,
75
+ useCases: 0,
76
+ providers: 0,
77
+ mocks: 0,
78
+ specs: 0
79
+ };
80
+
81
+ const schemas =
82
+ (analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
83
+ ?.schemas || {};
84
+
85
+ // 1. Generate Models, Entities and Mappers from Schemas
86
+ Object.keys(schemas).forEach((schemaName) => {
87
+ const baseName = schemaName.replace(/Dto$/, '');
88
+
89
+ const schemaObj = schemas[schemaName] as OpenApiSchema;
90
+ const rawProperties = schemaObj.properties || {};
91
+ const requiredProps: string[] = schemaObj.required || [];
92
+
93
+ const varsMap = Object.keys(rawProperties).map((k) => {
94
+ let tsType = mapSwaggerTypeToTs(rawProperties[k].type);
95
+ if (rawProperties[k].$ref) {
96
+ tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
97
+ } else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
98
+ tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
99
+ }
100
+ return {
101
+ name: k,
102
+ dataType: tsType,
103
+ description: rawProperties[k].description || '',
104
+ required: requiredProps.includes(k)
105
+ };
106
+ });
107
+
108
+ // Collect imports for types referenced via $ref in properties
109
+ const referencedTypes = new Set<string>();
110
+ Object.values(rawProperties).forEach((prop) => {
111
+ if (prop.$ref) {
112
+ referencedTypes.add(prop.$ref.split('/').pop() || '');
113
+ } else if (prop.type === 'array' && prop.items?.$ref) {
114
+ referencedTypes.add(prop.items.$ref.split('/').pop() || '');
115
+ }
116
+ });
117
+ const modelImports = [...referencedTypes]
118
+ .filter(Boolean)
119
+ .map((name) => ({ classname: name, classFilename: toCamelCase(name) }));
120
+
121
+ const modelViewData = {
122
+ models: [
123
+ {
124
+ model: {
125
+ classname: baseName,
126
+ classFilename: toCamelCase(baseName),
127
+ classVarName: toCamelCase(baseName),
128
+ description: schemaObj.description || '',
129
+ imports: modelImports,
130
+ vars: varsMap
131
+ }
132
+ }
133
+ ],
134
+ allModels: [{ model: { vars: varsMap } }]
135
+ };
136
+
137
+ const mapperViewData = {
138
+ ...modelViewData,
139
+ apiInfo: {
140
+ apis: [
141
+ {
142
+ operations: {
143
+ classname: baseName,
144
+ classFilename: toCamelCase(baseName),
145
+ classVarName: toCamelCase(baseName)
146
+ }
147
+ }
148
+ ]
149
+ }
150
+ };
151
+
152
+ // Model (Entities)
153
+ const modelTemplatePath = path.join(templatesDir, 'model-entity.mustache');
154
+ if (fs.existsSync(modelTemplatePath)) {
155
+ const template = fs.readFileSync(modelTemplatePath, 'utf8');
156
+ const output = mustache.render(template, modelViewData);
157
+ const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`);
158
+ fs.writeFileSync(destPath, output);
159
+ generatedCount.models++;
160
+ logDetail('generate', `model-entity → ${path.relative(process.cwd(), destPath)}`);
161
+ }
162
+
163
+ // Mapper
164
+ const mapperTemplatePath = path.join(templatesDir, 'mapper.mustache');
165
+ if (fs.existsSync(mapperTemplatePath)) {
166
+ const template = fs.readFileSync(mapperTemplatePath, 'utf8');
167
+ const output = mustache.render(template, mapperViewData);
168
+ const destPath = path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.ts`);
169
+ fs.writeFileSync(destPath, output);
170
+ generatedCount.mappers++;
171
+ }
172
+
173
+ // DTO mock — values resolved from raw schema (example, format, type)
174
+ const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
175
+ name: k,
176
+ mockValue: resolveMockValue(k, rawProperties[k], 'dto')
177
+ }));
178
+ const dtoMockImports = [...referencedTypes]
179
+ .filter(Boolean)
180
+ .map((name) => ({ classname: name, classFilename: toCamelCase(name) }));
181
+
182
+ const dtoMockViewData = {
183
+ models: [
184
+ {
185
+ model: {
186
+ classname: baseName,
187
+ classFilename: toCamelCase(baseName),
188
+ classVarName: toCamelCase(baseName),
189
+ mockImports: dtoMockImports,
190
+ vars: dtoMockVarsMap
191
+ }
192
+ }
193
+ ]
194
+ };
195
+
196
+ renderTemplate(
197
+ templatesDir,
198
+ 'dto.mock.mustache',
199
+ dtoMockViewData,
200
+ path.join(outputDir, 'data/dtos', `${toCamelCase(baseName)}.dto.mock.ts`),
201
+ generatedCount,
202
+ 'mocks'
203
+ );
204
+
205
+ // Model mock — delegates to mapper + DTO mock (no property values needed)
206
+ renderTemplate(
207
+ templatesDir,
208
+ 'model.mock.mustache',
209
+ modelViewData,
210
+ path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.mock.ts`),
211
+ generatedCount,
212
+ 'mocks'
213
+ );
214
+
215
+ // Model spec
216
+ renderTemplate(
217
+ templatesDir,
218
+ 'model-entity.spec.mustache',
219
+ modelViewData,
220
+ path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.spec.ts`),
221
+ generatedCount,
222
+ 'specs'
223
+ );
224
+
225
+ // Mapper spec
226
+ renderTemplate(
227
+ templatesDir,
228
+ 'mapper.spec.mustache',
229
+ mapperViewData,
230
+ path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.spec.ts`),
231
+ generatedCount,
232
+ 'specs'
233
+ );
234
+ });
235
+
236
+ // 2. Generate Use Cases and Repositories from Paths/Tags
237
+ const tagsMap: Record<string, TagOperation[]> = {};
238
+
239
+ Object.keys(analysis.paths).forEach((pathKey) => {
240
+ const pathObj = analysis.paths[pathKey] as Record<string, unknown>;
241
+ Object.keys(pathObj).forEach((method) => {
242
+ const op = pathObj[method] as OpenApiOperation;
243
+ if (op.tags && op.tags.length > 0) {
244
+ const tag = op.tags[0];
245
+ if (!tagsMap[tag]) tagsMap[tag] = [];
246
+
247
+ const allParams = (op.parameters || []).map((p) => ({
248
+ paramName: p.name,
249
+ dataType: mapSwaggerTypeToTs(p.schema?.type || ''),
250
+ description: p.description || '',
251
+ required: p.required,
252
+ testValue: resolveTestParamValue(mapSwaggerTypeToTs(p.schema?.type || ''))
253
+ }));
254
+
255
+ if (op.requestBody) {
256
+ let bodyType = 'unknown';
257
+ const content = op.requestBody.content?.['application/json']?.schema;
258
+ if (content) {
259
+ if (content.$ref) bodyType = content.$ref.split('/').pop() || 'unknown';
260
+ else if (content.type) bodyType = mapSwaggerTypeToTs(content.type);
261
+ }
262
+ allParams.push({
263
+ paramName: 'body',
264
+ dataType: bodyType,
265
+ description: op.requestBody.description || '',
266
+ required: true,
267
+ testValue: resolveTestParamValue(bodyType)
268
+ });
269
+ }
270
+
271
+ let returnType = 'void';
272
+ let returnBaseType = 'void';
273
+ let isListContainer = false;
274
+ const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
275
+ const responseSchema =
276
+ successCode !== undefined
277
+ ? op.responses?.[successCode]?.content?.['application/json']?.schema
278
+ : undefined;
279
+ if (responseSchema) {
280
+ if (responseSchema.$ref) {
281
+ returnType = responseSchema.$ref.split('/').pop() || 'unknown';
282
+ returnBaseType = returnType;
283
+ } else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
284
+ returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
285
+ returnType = `${returnBaseType}[]`;
286
+ isListContainer = true;
287
+ }
288
+ }
289
+
290
+ const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
291
+ const hasBodyParam = !!op.requestBody;
292
+
293
+ tagsMap[tag].push({
294
+ nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
295
+ summary: op.summary || '',
296
+ notes: op.description || '',
297
+ httpMethod: method.toLowerCase(),
298
+ uppercaseHttpMethod: method.toUpperCase(),
299
+ path: pathKey,
300
+ allParams: allParams.map((p, i: number) => ({
301
+ ...p,
302
+ '-last': i === allParams.length - 1
303
+ })),
304
+ hasQueryParams,
305
+ queryParams: (op.parameters || [])
306
+ .filter((p) => p.in === 'query')
307
+ .map((p, i: number, arr: unknown[]) => ({
308
+ paramName: p.name,
309
+ '-last': i === arr.length - 1
310
+ })),
311
+ hasBodyParam,
312
+ bodyParam: 'body',
313
+ hasOptions: hasQueryParams || hasBodyParam,
314
+ hasBothParamsAndBody: hasQueryParams && hasBodyParam,
315
+ returnType: returnType !== 'void' ? returnType : false,
316
+ returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
317
+ returnTypeVarName: returnType !== 'void' ? toCamelCase(returnType) : false,
318
+ returnBaseTypeVarName: returnBaseType !== 'void' ? toCamelCase(returnBaseType) : false,
319
+ isListContainer: isListContainer,
320
+ vendorExtensions: {}
321
+ });
322
+ }
323
+ });
324
+ });
325
+
326
+ if (Object.keys(selectionFilter).length > 0) {
327
+ Object.keys(tagsMap).forEach((tag) => {
328
+ if (!selectionFilter[tag]) {
329
+ delete tagsMap[tag];
330
+ } else {
331
+ tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
332
+ if (tagsMap[tag].length === 0) delete tagsMap[tag];
333
+ }
334
+ });
335
+ }
336
+
337
+ // Generate per tag
338
+ Object.keys(tagsMap).forEach((tag) => {
339
+ const returnImports: { classname: string; classFilename: string; classVarName: string }[] = [];
340
+ const paramImports: { classname: string; classFilename: string; classVarName: string }[] = [];
341
+
342
+ Object.keys(schemas).forEach((s) => {
343
+ const usedAsReturn = tagsMap[tag].some(
344
+ (op) => op.returnType === s || op.returnType === `${s}[]`
345
+ );
346
+ const usedAsParam = tagsMap[tag].some((op) =>
347
+ op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`)
348
+ );
349
+
350
+ const entry = { classname: s, classFilename: toCamelCase(s), classVarName: toCamelCase(s) };
351
+
352
+ if (usedAsReturn) {
353
+ returnImports.push(entry);
354
+ } else if (usedAsParam) {
355
+ // Param-only types: entity import needed for method signatures, but no Dto or Mapper
356
+ paramImports.push(entry);
357
+ }
358
+ });
359
+
360
+ const apiViewData = {
361
+ apiInfo: {
362
+ apis: [
363
+ {
364
+ operations: {
365
+ classname: tag,
366
+ classFilename: toCamelCase(tag),
367
+ classVarName: toCamelCase(tag),
368
+ constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
369
+ operation: tagsMap[tag],
370
+ // All entity imports (return + param) — for contracts and use-cases
371
+ imports: [...returnImports, ...paramImports],
372
+ // Return-type-only imports — for repo impl (Dto + Entity + Mapper)
373
+ returnImports,
374
+ // Param-only imports — for repo impl (Entity only, no Dto/Mapper)
375
+ paramImports,
376
+ // Environment API key for the repository base URL (e.g. "aprovalmApi")
377
+ environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
378
+ }
379
+ }
380
+ ]
381
+ }
382
+ };
383
+
384
+ renderTemplate(
385
+ templatesDir,
386
+ 'api.use-cases.contract.mustache',
387
+ apiViewData,
388
+ path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.contract.ts`),
389
+ generatedCount,
390
+ 'useCases'
391
+ );
392
+
393
+ renderTemplate(
394
+ templatesDir,
395
+ 'api.use-cases.impl.mustache',
396
+ apiViewData,
397
+ path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.ts`),
398
+ generatedCount,
399
+ 'useCases'
400
+ );
401
+
402
+ renderTemplate(
403
+ templatesDir,
404
+ 'api.repository.contract.mustache',
405
+ apiViewData,
406
+ path.join(outputDir, 'domain/repositories', `${toCamelCase(tag)}.repository.contract.ts`),
407
+ generatedCount,
408
+ 'repositories'
409
+ );
410
+
411
+ renderTemplate(
412
+ templatesDir,
413
+ 'api.repository.impl.mustache',
414
+ apiViewData,
415
+ path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.ts`),
416
+ generatedCount,
417
+ 'repositories'
418
+ );
419
+
420
+ renderTemplate(
421
+ templatesDir,
422
+ 'use-cases.provider.mustache',
423
+ apiViewData,
424
+ path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.ts`),
425
+ generatedCount,
426
+ 'providers'
427
+ );
428
+
429
+ renderTemplate(
430
+ templatesDir,
431
+ 'repository.provider.mustache',
432
+ apiViewData,
433
+ path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.ts`),
434
+ generatedCount,
435
+ 'providers'
436
+ );
437
+
438
+ // Mocks — repository impl, use-cases impl, repository provider, use-cases provider
439
+ renderTemplate(
440
+ templatesDir,
441
+ 'api.repository.impl.mock.mustache',
442
+ apiViewData,
443
+ path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.mock.ts`),
444
+ generatedCount,
445
+ 'mocks'
446
+ );
447
+
448
+ renderTemplate(
449
+ templatesDir,
450
+ 'api.use-cases.mock.mustache',
451
+ apiViewData,
452
+ path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.mock.ts`),
453
+ generatedCount,
454
+ 'mocks'
455
+ );
456
+
457
+ renderTemplate(
458
+ templatesDir,
459
+ 'repository.provider.mock.mustache',
460
+ apiViewData,
461
+ path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.mock.ts`),
462
+ generatedCount,
463
+ 'mocks'
464
+ );
465
+
466
+ renderTemplate(
467
+ templatesDir,
468
+ 'use-cases.provider.mock.mustache',
469
+ apiViewData,
470
+ path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.mock.ts`),
471
+ generatedCount,
472
+ 'mocks'
473
+ );
474
+
475
+ // Repository impl spec
476
+ renderTemplate(
477
+ templatesDir,
478
+ 'api.repository.impl.spec.mustache',
479
+ apiViewData,
480
+ path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.spec.ts`),
481
+ generatedCount,
482
+ 'specs'
483
+ );
484
+
485
+ // Use-cases impl spec
486
+ renderTemplate(
487
+ templatesDir,
488
+ 'api.use-cases.impl.spec.mustache',
489
+ apiViewData,
490
+ path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.spec.ts`),
491
+ generatedCount,
492
+ 'specs'
493
+ );
494
+ });
495
+
496
+ logSuccess(
497
+ `${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks, ${generatedCount.specs} Specs generated`
498
+ );
499
+ return generatedCount;
500
+ }
501
+
502
+ /** Renders a Mustache template and increments the corresponding counter. */
503
+ function renderTemplate(
504
+ templatesDir: string,
505
+ templateName: string,
506
+ viewData: unknown,
507
+ destPath: string,
508
+ counter: GeneratedCount,
509
+ key: keyof GeneratedCount
510
+ ): void {
511
+ const templatePath = path.join(templatesDir, templateName);
512
+ if (fs.existsSync(templatePath)) {
513
+ const template = fs.readFileSync(templatePath, 'utf8');
514
+ const output = mustache.render(template, viewData);
515
+ fs.writeFileSync(destPath, output);
516
+ counter[key]++;
517
+ logDetail(
518
+ 'generate',
519
+ `${templateName.replace('.mustache', '')} → ${path.relative(process.cwd(), destPath)}`
520
+ );
521
+ }
522
+ }
523
+
524
+ /** Resolves a simple test value literal for a given TypeScript type. */
525
+ function resolveTestParamValue(dataType: string): string {
526
+ switch (dataType) {
527
+ case 'string':
528
+ return "'test'";
529
+ case 'number':
530
+ return '1';
531
+ case 'boolean':
532
+ return 'true';
533
+ default:
534
+ if (dataType.endsWith('[]')) return '[]';
535
+ return '{} as any';
536
+ }
537
+ }