@0kmpo/openapi-clean-arch-generator 1.3.13 → 1.3.15

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 CHANGED
@@ -76,7 +76,10 @@ Options:
76
76
  -o, --output <dir> Output directory [default: ./src/app]
77
77
  -t, --templates <dir> Custom templates directory [default: ./templates]
78
78
  -s, --select-endpoints Interactively select tags and endpoints to generate
79
+ -c, --config <file> Use a JSON configuration file (skips interactive prompts)
80
+ --init-config [file] Generate a JSON configuration file instead of generating code
79
81
  --skip-install Skip dependency installation
82
+ --skip-lint Skip post-generation linting and formatting
80
83
  --dry-run Simulate without writing files
81
84
  -h, --help Show help
82
85
  ```
@@ -96,6 +99,15 @@ generate-clean-arch -i api.yaml -t ./my-templates
96
99
  # Dry run (no files written)
97
100
  generate-clean-arch -i swagger.yaml --dry-run
98
101
 
102
+ # Skip linting after generation
103
+ generate-clean-arch -i swagger.yaml --skip-lint
104
+
105
+ # Generate a config file to reuse later
106
+ generate-clean-arch --init-config generation-config.json
107
+
108
+ # Run using a config file (no interactive prompts)
109
+ generate-clean-arch -c generation-config.json
110
+
99
111
  # Full example with all options
100
112
  generate-clean-arch -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates
101
113
  ```
package/dist/main.js CHANGED
@@ -17,6 +17,7 @@ const clean_arch_generator_1 = require("./src/generators/clean-arch.generator");
17
17
  const report_generator_1 = require("./src/generators/report.generator");
18
18
  const lint_generator_1 = require("./src/generators/lint.generator");
19
19
  const environment_finder_1 = require("./src/utils/environment-finder");
20
+ const example_validator_1 = require("./src/utils/example-validator");
20
21
  const prompt_1 = require("./src/utils/prompt");
21
22
  const config_1 = require("./src/utils/config");
22
23
  const package_json_1 = __importDefault(require("./package.json"));
@@ -109,6 +110,7 @@ async function main() {
109
110
  return;
110
111
  }
111
112
  (0, filesystem_1.createDirectoryStructure)(options.output);
113
+ (0, example_validator_1.clearExampleMismatches)();
112
114
  // ── SELECTION: tags and endpoints ─────────────────────────────────────────
113
115
  let selectionFilter = {};
114
116
  let tagApiKeyMap;
@@ -150,10 +152,27 @@ async function main() {
150
152
  }
151
153
  // ──────────────────────────────────────────────────────────────────────────
152
154
  const tempDir = (0, dto_generator_1.generateCode)(options.input, options.templates);
153
- (0, dto_generator_1.organizeFiles)(tempDir, options.output);
155
+ // Compute schema→tag map before organizeFiles so DTOs land in the right subfolder
156
+ const tagsMapForSchema = (0, clean_arch_generator_1.buildTagsMapFromAnalysis)(analysis, selectionFilter);
157
+ const schemaTagMap = (0, clean_arch_generator_1.buildSchemaTagMap)(analysis.swagger.components
158
+ ?.schemas || {}, tagsMapForSchema);
159
+ (0, dto_generator_1.organizeFiles)(tempDir, options.output, schemaTagMap);
154
160
  (0, dto_generator_1.addDtoImports)(options.output);
155
- (0, clean_arch_generator_1.generateCleanArchitecture)(analysis, options.output, options.templates, tagApiKeyMap, selectionFilter);
161
+ (0, clean_arch_generator_1.generateCleanArchitecture)(analysis, options.output, options.templates, tagApiKeyMap, selectionFilter, schemaTagMap);
156
162
  (0, filesystem_1.cleanup)(tempDir);
163
+ // ── EXAMPLE/TYPE MISMATCH WARNINGS ─────────────────────────────────────────
164
+ const mismatches = (0, example_validator_1.getExampleMismatches)();
165
+ if (mismatches.length > 0) {
166
+ console.log('');
167
+ (0, logger_1.logWarning)(`${mismatches.length} example/type mismatch(es) detected in OpenAPI schemas:`);
168
+ for (const m of mismatches) {
169
+ const action = m.action === 'coerced'
170
+ ? `→ coerced to ${JSON.stringify(m.coercedValue)}`
171
+ : '→ example ignored, using type default';
172
+ (0, logger_1.logWarning)(` ${m.schemaName}.${m.propertyName}: type '${m.declaredType}' but example is ${m.exampleJsType} (${JSON.stringify(m.exampleValue)}) ${action}`);
173
+ (0, logger_1.logDetail)('VALIDATE', `${m.schemaName}.${m.propertyName}: declared=${m.declaredType} example=${JSON.stringify(m.exampleValue)} (${m.exampleJsType}) action=${m.action}`);
174
+ }
175
+ }
157
176
  const noLintResult = {
158
177
  prettier: { ran: false, filesFormatted: 0 },
159
178
  eslint: { ran: false, filesFixed: 0 }
@@ -170,6 +189,9 @@ async function main() {
170
189
  console.log(` - Use Cases: ${report.structure.useCases}`);
171
190
  console.log(` - Providers: ${report.structure.providers}`);
172
191
  console.log(` - Mocks: ${report.structure.mocks}`);
192
+ if (report.warnings.total > 0) {
193
+ console.log(`\n ${logger_1.colors.yellow}⚠️ ${report.warnings.total} example/type mismatch(es) (see above)${logger_1.colors.reset}`);
194
+ }
173
195
  console.log(`\n📁 Files generated in: ${logger_1.colors.cyan}${options.output}${logger_1.colors.reset}\n`);
174
196
  }
175
197
  main().catch((error) => {
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@0kmpo/openapi-clean-arch-generator",
3
+ "version": "1.3.15",
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
+ "files": [
43
+ "dist/main.js",
44
+ "dist/package.json",
45
+ "dist/src/",
46
+ "dist/templates/",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "dependencies": {
51
+ "chalk": "^4.1.2",
52
+ "commander": "^11.1.0",
53
+ "fs-extra": "^11.2.0",
54
+ "js-yaml": "^4.1.0",
55
+ "mustache": "^4.2.0",
56
+ "prompts": "^2.4.2"
57
+ },
58
+ "engines": {
59
+ "bun": ">=1.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@eslint/js": "^10.0.1",
63
+ "@types/fs-extra": "^11.0.4",
64
+ "@types/js-yaml": "^4.0.9",
65
+ "@types/mustache": "^4.2.6",
66
+ "@types/node": "^25.5.0",
67
+ "@types/prompts": "^2.4.9",
68
+ "@typescript-eslint/eslint-plugin": "^8.57.1",
69
+ "@typescript-eslint/parser": "^8.57.1",
70
+ "eslint": "^10.1.0",
71
+ "eslint-config-prettier": "^10.1.8",
72
+ "eslint-formatter-unix": "^9.0.1",
73
+ "eslint-plugin-prettier": "^5.5.5",
74
+ "prettier": "^3.8.1",
75
+ "typescript": "^5.9.3",
76
+ "typescript-eslint": "^8.57.1"
77
+ }
78
+ }
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.extractTagsFromAnalysis = extractTagsFromAnalysis;
7
7
  exports.extractTagsWithOperations = extractTagsWithOperations;
8
+ exports.buildTagsMapFromAnalysis = buildTagsMapFromAnalysis;
9
+ exports.buildSchemaTagMap = buildSchemaTagMap;
8
10
  exports.generateCleanArchitecture = generateCleanArchitecture;
9
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
10
12
  const path_1 = __importDefault(require("path"));
@@ -57,8 +59,139 @@ function extractTagsWithOperations(analysis) {
57
59
  });
58
60
  return [...map.values()];
59
61
  }
62
+ /**
63
+ * Builds and returns the tagsMap from the swagger analysis, applying the optional selection filter.
64
+ * Exported so callers (e.g. main.ts) can compute it before organizeFiles runs.
65
+ */
66
+ function buildTagsMapFromAnalysis(analysis, selectionFilter = {}) {
67
+ const tagsMap = {};
68
+ Object.keys(analysis.paths).forEach((pathKey) => {
69
+ const pathObj = analysis.paths[pathKey];
70
+ Object.keys(pathObj).forEach((method) => {
71
+ const op = pathObj[method];
72
+ if (op.tags && op.tags.length > 0) {
73
+ const tag = op.tags[0];
74
+ if (!tagsMap[tag])
75
+ tagsMap[tag] = [];
76
+ const allParams = (op.parameters || []).map((p) => ({
77
+ paramName: p.name,
78
+ dataType: (0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''),
79
+ description: p.description || '',
80
+ required: p.required,
81
+ testValue: resolveTestParamValue((0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''))
82
+ }));
83
+ if (op.requestBody) {
84
+ let bodyType = 'unknown';
85
+ const content = op.requestBody.content?.['application/json']?.schema;
86
+ if (content) {
87
+ if (content.$ref)
88
+ bodyType = content.$ref.split('/').pop() || 'unknown';
89
+ else if (content.type)
90
+ bodyType = (0, type_mapper_1.mapSwaggerTypeToTs)(content.type);
91
+ }
92
+ allParams.push({
93
+ paramName: 'body',
94
+ dataType: bodyType,
95
+ description: op.requestBody.description || '',
96
+ required: true,
97
+ testValue: resolveTestParamValue(bodyType)
98
+ });
99
+ }
100
+ let returnType = 'void';
101
+ let returnBaseType = 'void';
102
+ let isListContainer = false;
103
+ const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
104
+ const responseSchema = successCode !== undefined
105
+ ? op.responses?.[successCode]?.content?.['application/json']?.schema
106
+ : undefined;
107
+ if (responseSchema) {
108
+ if (responseSchema.$ref) {
109
+ returnType = responseSchema.$ref.split('/').pop() || 'unknown';
110
+ returnBaseType = returnType;
111
+ }
112
+ else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
113
+ returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
114
+ returnType = `${returnBaseType}[]`;
115
+ isListContainer = true;
116
+ }
117
+ }
118
+ const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
119
+ const hasBodyParam = !!op.requestBody;
120
+ // Sort: required params first, optional params last (TypeScript requirement)
121
+ allParams.sort((a, b) => {
122
+ if (a.required === b.required)
123
+ return 0;
124
+ return a.required ? -1 : 1;
125
+ });
126
+ tagsMap[tag].push({
127
+ nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
128
+ summary: op.summary || '',
129
+ notes: op.description || '',
130
+ httpMethod: method.toLowerCase(),
131
+ uppercaseHttpMethod: method.toUpperCase(),
132
+ path: pathKey,
133
+ allParams: allParams.map((p, i) => ({
134
+ ...p,
135
+ '-last': i === allParams.length - 1
136
+ })),
137
+ hasQueryParams,
138
+ queryParams: (op.parameters || [])
139
+ .filter((p) => p.in === 'query')
140
+ .map((p, i, arr) => ({
141
+ paramName: p.name,
142
+ '-last': i === arr.length - 1
143
+ })),
144
+ hasBodyParam,
145
+ bodyParam: 'body',
146
+ hasOptions: hasQueryParams || hasBodyParam,
147
+ hasBothParamsAndBody: hasQueryParams && hasBodyParam,
148
+ returnType: returnType !== 'void' ? returnType : false,
149
+ returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
150
+ returnTypeVarName: returnType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnType) : false,
151
+ returnBaseTypeVarName: returnBaseType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnBaseType) : false,
152
+ isListContainer: isListContainer,
153
+ vendorExtensions: {}
154
+ });
155
+ }
156
+ });
157
+ });
158
+ if (Object.keys(selectionFilter).length > 0) {
159
+ Object.keys(tagsMap).forEach((tag) => {
160
+ if (!selectionFilter[tag]) {
161
+ delete tagsMap[tag];
162
+ }
163
+ else {
164
+ tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
165
+ if (tagsMap[tag].length === 0)
166
+ delete tagsMap[tag];
167
+ }
168
+ });
169
+ }
170
+ return tagsMap;
171
+ }
172
+ /**
173
+ * Maps each schema basename to the tag subfolder it belongs to.
174
+ * Schemas used by exactly one tag → that tag's camelCase name.
175
+ * Schemas used by 0 or multiple tags → 'shared'.
176
+ */
177
+ function buildSchemaTagMap(schemas, tagsMap) {
178
+ const result = {};
179
+ Object.keys(schemas).forEach((schemaName) => {
180
+ const baseName = schemaName.replace(/Dto$/, '');
181
+ const tagsUsing = [];
182
+ Object.keys(tagsMap).forEach((tag) => {
183
+ const used = tagsMap[tag].some((op) => op.returnType === baseName ||
184
+ op.returnType === `${baseName}[]` ||
185
+ op.allParams.some((p) => p.dataType === baseName || p.dataType === `${baseName}[]`));
186
+ if (used)
187
+ tagsUsing.push(tag);
188
+ });
189
+ result[baseName] = tagsUsing.length === 1 ? (0, name_formatter_1.toCamelCase)(tagsUsing[0]) : 'shared';
190
+ });
191
+ return result;
192
+ }
60
193
  /** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
61
- function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyMap = {}, selectionFilter = {}) {
194
+ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyMap = {}, selectionFilter = {}, precomputedSchemaTagMap = {}) {
62
195
  (0, logger_1.logStep)('Generating Clean Architecture artefacts using Mustache...');
63
196
  const generatedCount = {
64
197
  models: 0,
@@ -71,14 +204,22 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
71
204
  };
72
205
  const schemas = analysis.swagger.components
73
206
  ?.schemas || {};
207
+ // Build tagsMap first — needed to compute schemaTagMap before the schema loop
208
+ const tagsMap = buildTagsMapFromAnalysis(analysis, selectionFilter);
209
+ // Map each schema basename → tag subfolder ('shared' if used by 0 or >1 tags)
210
+ const schemaTagMap = Object.keys(precomputedSchemaTagMap).length > 0
211
+ ? precomputedSchemaTagMap
212
+ : buildSchemaTagMap(schemas, tagsMap);
74
213
  // 1. Generate Models, Entities and Mappers from Schemas
75
214
  Object.keys(schemas).forEach((schemaName) => {
76
215
  const baseName = schemaName.replace(/Dto$/, '');
216
+ const tagFilename = schemaTagMap[baseName] || 'shared';
77
217
  const schemaObj = schemas[schemaName];
78
218
  const rawProperties = schemaObj.properties || {};
79
219
  const requiredProps = schemaObj.required || [];
80
220
  const varsMap = Object.keys(rawProperties).map((k) => {
81
221
  let tsType = (0, type_mapper_1.mapSwaggerTypeToTs)(rawProperties[k].type);
222
+ const isInlineObject = rawProperties[k].type === 'object' && !rawProperties[k].$ref;
82
223
  if (rawProperties[k].$ref) {
83
224
  tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
84
225
  }
@@ -86,10 +227,12 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
86
227
  tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
87
228
  }
88
229
  return {
89
- name: k,
230
+ name: (0, name_formatter_1.safePropertyName)(k),
231
+ originalName: k,
90
232
  dataType: tsType,
91
233
  description: rawProperties[k].description || '',
92
- required: requiredProps.includes(k)
234
+ required: requiredProps.includes(k),
235
+ hasMockValue: !isInlineObject
93
236
  };
94
237
  });
95
238
  // Collect imports for types referenced via $ref in properties
@@ -102,10 +245,13 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
102
245
  referencedTypes.add(prop.items.$ref.split('/').pop() || '');
103
246
  }
104
247
  });
105
- const modelImports = [...referencedTypes]
106
- .filter(Boolean)
107
- .map((name) => ({ classname: name, classFilename: (0, name_formatter_1.toCamelCase)(name) }));
248
+ const modelImports = [...referencedTypes].filter(Boolean).map((name) => ({
249
+ classname: name,
250
+ classFilename: (0, name_formatter_1.toCamelCase)(name),
251
+ tagFilename: schemaTagMap[name] || 'shared'
252
+ }));
108
253
  const modelViewData = {
254
+ tagFilename,
109
255
  models: [
110
256
  {
111
257
  model: {
@@ -128,7 +274,8 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
128
274
  operations: {
129
275
  classname: baseName,
130
276
  classFilename: (0, name_formatter_1.toCamelCase)(baseName),
131
- classVarName: (0, name_formatter_1.toCamelCase)(baseName)
277
+ classVarName: (0, name_formatter_1.toCamelCase)(baseName),
278
+ tagFilename
132
279
  }
133
280
  }
134
281
  ]
@@ -139,7 +286,8 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
139
286
  if (fs_extra_1.default.existsSync(modelTemplatePath)) {
140
287
  const template = fs_extra_1.default.readFileSync(modelTemplatePath, 'utf8');
141
288
  const output = mustache_1.default.render(template, modelViewData);
142
- const destPath = path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(baseName)}.model.ts`);
289
+ const destPath = path_1.default.join(outputDir, 'entities/models', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.model.ts`);
290
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
143
291
  fs_extra_1.default.writeFileSync(destPath, output);
144
292
  generatedCount.models++;
145
293
  (0, logger_1.logDetail)('generate', `model-entity → ${path_1.default.relative(process.cwd(), destPath)}`);
@@ -149,19 +297,29 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
149
297
  if (fs_extra_1.default.existsSync(mapperTemplatePath)) {
150
298
  const template = fs_extra_1.default.readFileSync(mapperTemplatePath, 'utf8');
151
299
  const output = mustache_1.default.render(template, mapperViewData);
152
- const destPath = path_1.default.join(outputDir, 'data/mappers', `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.ts`);
300
+ const destPath = path_1.default.join(outputDir, 'data/mappers', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.ts`);
301
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
153
302
  fs_extra_1.default.writeFileSync(destPath, output);
154
303
  generatedCount.mappers++;
155
304
  }
156
305
  // DTO mock — values resolved from raw schema (example, format, type)
157
306
  const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
158
- name: k,
159
- mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto')
307
+ name: (0, name_formatter_1.safePropertyName)(k),
308
+ mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto', schemaName)
160
309
  }));
161
- const dtoMockImports = [...referencedTypes]
162
- .filter(Boolean)
163
- .map((name) => ({ classname: name, classFilename: (0, name_formatter_1.toCamelCase)(name) }));
310
+ const dtoMockImports = [...referencedTypes].filter(Boolean).map((name) => {
311
+ const targetTag = schemaTagMap[name] || 'shared';
312
+ const targetFile = `${(0, name_formatter_1.toCamelCase)(name)}.dto.mock`;
313
+ const importPath = targetTag === tagFilename ? `./${targetFile}` : `../${targetTag}/${targetFile}`;
314
+ return {
315
+ classname: name,
316
+ classFilename: (0, name_formatter_1.toCamelCase)(name),
317
+ tagFilename: targetTag,
318
+ importPath
319
+ };
320
+ });
164
321
  const dtoMockViewData = {
322
+ tagFilename,
165
323
  models: [
166
324
  {
167
325
  model: {
@@ -174,120 +332,29 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
174
332
  }
175
333
  ]
176
334
  };
177
- renderTemplate(templatesDir, 'dto.mock.mustache', dtoMockViewData, path_1.default.join(outputDir, 'data/dtos', `${(0, name_formatter_1.toCamelCase)(baseName)}.dto.mock.ts`), generatedCount, 'mocks');
335
+ renderTemplate(templatesDir, 'dto.mock.mustache', dtoMockViewData, path_1.default.join(outputDir, 'data/dtos', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.dto.mock.ts`), generatedCount, 'mocks');
178
336
  // Model mock — delegates to mapper + DTO mock (no property values needed)
179
- renderTemplate(templatesDir, 'model.mock.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(baseName)}.model.mock.ts`), generatedCount, 'mocks');
337
+ renderTemplate(templatesDir, 'model.mock.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.model.mock.ts`), generatedCount, 'mocks');
180
338
  // Model spec
181
- renderTemplate(templatesDir, 'model-entity.spec.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(baseName)}.model.spec.ts`), generatedCount, 'specs');
339
+ renderTemplate(templatesDir, 'model-entity.spec.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.model.spec.ts`), generatedCount, 'specs');
182
340
  // Mapper spec
183
- renderTemplate(templatesDir, 'mapper.spec.mustache', mapperViewData, path_1.default.join(outputDir, 'data/mappers', `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.spec.ts`), generatedCount, 'specs');
341
+ renderTemplate(templatesDir, 'mapper.spec.mustache', mapperViewData, path_1.default.join(outputDir, 'data/mappers', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.spec.ts`), generatedCount, 'specs');
184
342
  });
185
343
  // 2. Generate Use Cases and Repositories from Paths/Tags
186
- const tagsMap = {};
187
- Object.keys(analysis.paths).forEach((pathKey) => {
188
- const pathObj = analysis.paths[pathKey];
189
- Object.keys(pathObj).forEach((method) => {
190
- const op = pathObj[method];
191
- if (op.tags && op.tags.length > 0) {
192
- const tag = op.tags[0];
193
- if (!tagsMap[tag])
194
- tagsMap[tag] = [];
195
- const allParams = (op.parameters || []).map((p) => ({
196
- paramName: p.name,
197
- dataType: (0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''),
198
- description: p.description || '',
199
- required: p.required,
200
- testValue: resolveTestParamValue((0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''))
201
- }));
202
- if (op.requestBody) {
203
- let bodyType = 'unknown';
204
- const content = op.requestBody.content?.['application/json']?.schema;
205
- if (content) {
206
- if (content.$ref)
207
- bodyType = content.$ref.split('/').pop() || 'unknown';
208
- else if (content.type)
209
- bodyType = (0, type_mapper_1.mapSwaggerTypeToTs)(content.type);
210
- }
211
- allParams.push({
212
- paramName: 'body',
213
- dataType: bodyType,
214
- description: op.requestBody.description || '',
215
- required: true,
216
- testValue: resolveTestParamValue(bodyType)
217
- });
218
- }
219
- let returnType = 'void';
220
- let returnBaseType = 'void';
221
- let isListContainer = false;
222
- const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
223
- const responseSchema = successCode !== undefined
224
- ? op.responses?.[successCode]?.content?.['application/json']?.schema
225
- : undefined;
226
- if (responseSchema) {
227
- if (responseSchema.$ref) {
228
- returnType = responseSchema.$ref.split('/').pop() || 'unknown';
229
- returnBaseType = returnType;
230
- }
231
- else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
232
- returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
233
- returnType = `${returnBaseType}[]`;
234
- isListContainer = true;
235
- }
236
- }
237
- const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
238
- const hasBodyParam = !!op.requestBody;
239
- tagsMap[tag].push({
240
- nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
241
- summary: op.summary || '',
242
- notes: op.description || '',
243
- httpMethod: method.toLowerCase(),
244
- uppercaseHttpMethod: method.toUpperCase(),
245
- path: pathKey,
246
- allParams: allParams.map((p, i) => ({
247
- ...p,
248
- '-last': i === allParams.length - 1
249
- })),
250
- hasQueryParams,
251
- queryParams: (op.parameters || [])
252
- .filter((p) => p.in === 'query')
253
- .map((p, i, arr) => ({
254
- paramName: p.name,
255
- '-last': i === arr.length - 1
256
- })),
257
- hasBodyParam,
258
- bodyParam: 'body',
259
- hasOptions: hasQueryParams || hasBodyParam,
260
- hasBothParamsAndBody: hasQueryParams && hasBodyParam,
261
- returnType: returnType !== 'void' ? returnType : false,
262
- returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
263
- returnTypeVarName: returnType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnType) : false,
264
- returnBaseTypeVarName: returnBaseType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnBaseType) : false,
265
- isListContainer: isListContainer,
266
- vendorExtensions: {}
267
- });
268
- }
269
- });
270
- });
271
- if (Object.keys(selectionFilter).length > 0) {
272
- Object.keys(tagsMap).forEach((tag) => {
273
- if (!selectionFilter[tag]) {
274
- delete tagsMap[tag];
275
- }
276
- else {
277
- tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
278
- if (tagsMap[tag].length === 0)
279
- delete tagsMap[tag];
280
- }
281
- });
282
- }
283
344
  // Generate per tag
284
345
  Object.keys(tagsMap).forEach((tag) => {
346
+ const tagFilename = (0, name_formatter_1.toCamelCase)(tag);
285
347
  const returnImports = [];
286
348
  const paramImports = [];
287
349
  Object.keys(schemas).forEach((s) => {
288
350
  const usedAsReturn = tagsMap[tag].some((op) => op.returnType === s || op.returnType === `${s}[]`);
289
351
  const usedAsParam = tagsMap[tag].some((op) => op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`));
290
- const entry = { classname: s, classFilename: (0, name_formatter_1.toCamelCase)(s), classVarName: (0, name_formatter_1.toCamelCase)(s) };
352
+ const entry = {
353
+ classname: s,
354
+ classFilename: (0, name_formatter_1.toCamelCase)(s),
355
+ classVarName: (0, name_formatter_1.toCamelCase)(s),
356
+ tagFilename: schemaTagMap[s] || 'shared'
357
+ };
291
358
  if (usedAsReturn) {
292
359
  returnImports.push(entry);
293
360
  }
@@ -301,39 +368,35 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
301
368
  apis: [
302
369
  {
303
370
  operations: {
304
- classname: tag,
305
- classFilename: (0, name_formatter_1.toCamelCase)(tag),
306
- classVarName: (0, name_formatter_1.toCamelCase)(tag),
371
+ classname: (0, name_formatter_1.toPascalCase)(tag),
372
+ classFilename: tagFilename,
373
+ classVarName: tagFilename,
307
374
  constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
308
375
  operation: tagsMap[tag],
309
- // All entity imports (return + param) — for contracts and use-cases
310
376
  imports: [...returnImports, ...paramImports],
311
- // Return-type-only imports — for repo impl (Dto + Entity + Mapper)
312
377
  returnImports,
313
- // Param-only imports — for repo impl (Entity only, no Dto/Mapper)
314
378
  paramImports,
315
- // Environment API key for the repository base URL (e.g. "aprovalmApi")
316
379
  environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
317
380
  }
318
381
  }
319
382
  ]
320
383
  }
321
384
  };
322
- renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.contract.ts`), generatedCount, 'useCases');
323
- renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.impl.ts`), generatedCount, 'useCases');
324
- renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.contract.ts`), generatedCount, 'repositories');
325
- renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.impl.ts`), generatedCount, 'repositories');
326
- renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.provider.ts`), generatedCount, 'providers');
327
- renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.provider.ts`), generatedCount, 'providers');
328
- // Mocks — repository impl, use-cases impl, repository provider, use-cases provider
329
- renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.impl.mock.ts`), generatedCount, 'mocks');
330
- renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.mock.ts`), generatedCount, 'mocks');
331
- renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.provider.mock.ts`), generatedCount, 'mocks');
332
- renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.provider.mock.ts`), generatedCount, 'mocks');
385
+ renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.contract.ts`), generatedCount, 'useCases');
386
+ renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.impl.ts`), generatedCount, 'useCases');
387
+ renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', tagFilename, `${tagFilename}.repository.contract.ts`), generatedCount, 'repositories');
388
+ renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.ts`), generatedCount, 'repositories');
389
+ renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFilename, `${tagFilename}.use-cases.provider.ts`), generatedCount, 'providers');
390
+ renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFilename, `${tagFilename}.repository.provider.ts`), generatedCount, 'providers');
391
+ // Mocks
392
+ renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.mock.ts`), generatedCount, 'mocks');
393
+ renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.mock.ts`), generatedCount, 'mocks');
394
+ renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFilename, `${tagFilename}.repository.provider.mock.ts`), generatedCount, 'mocks');
395
+ renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFilename, `${tagFilename}.use-cases.provider.mock.ts`), generatedCount, 'mocks');
333
396
  // Repository impl spec
334
- renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.impl.spec.ts`), generatedCount, 'specs');
397
+ renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.spec.ts`), generatedCount, 'specs');
335
398
  // Use-cases impl spec
336
- renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.impl.spec.ts`), generatedCount, 'specs');
399
+ renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.impl.spec.ts`), generatedCount, 'specs');
337
400
  });
338
401
  (0, logger_1.logSuccess)(`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks, ${generatedCount.specs} Specs generated`);
339
402
  return generatedCount;
@@ -344,6 +407,7 @@ function renderTemplate(templatesDir, templateName, viewData, destPath, counter,
344
407
  if (fs_extra_1.default.existsSync(templatePath)) {
345
408
  const template = fs_extra_1.default.readFileSync(templatePath, 'utf8');
346
409
  const output = mustache_1.default.render(template, viewData);
410
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
347
411
  fs_extra_1.default.writeFileSync(destPath, output);
348
412
  counter[key]++;
349
413
  (0, logger_1.logDetail)('generate', `${templateName.replace('.mustache', '')} → ${path_1.default.relative(process.cwd(), destPath)}`);