@0kmpo/openapi-clean-arch-generator 1.3.14 → 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) => {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0kmpo/openapi-clean-arch-generator",
3
- "version": "1.3.14",
3
+ "version": "1.3.15",
4
4
  "description": "Angular Clean Architecture generator from OpenAPI/Swagger",
5
5
  "main": "dist/main.js",
6
6
  "bin": {
@@ -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)}`);
@@ -10,6 +10,7 @@ const child_process_1 = require("child_process");
10
10
  const fs_extra_1 = __importDefault(require("fs-extra"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const logger_1 = require("../utils/logger");
13
+ const name_formatter_1 = require("../utils/name-formatter");
13
14
  /** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
14
15
  function generateCode(swaggerFile, templatesDir) {
15
16
  (0, logger_1.logStep)('Generating code from OpenAPI spec...');
@@ -37,8 +38,8 @@ function generateCode(swaggerFile, templatesDir) {
37
38
  process.exit(1);
38
39
  }
39
40
  }
40
- /** Copies the generated DTOs from the temporary directory to the output directory. */
41
- function organizeFiles(tempDir, outputDir) {
41
+ /** Copies the generated DTOs from the temporary directory to the output directory, organised by tag subfolder. */
42
+ function organizeFiles(tempDir, outputDir, schemaTagMap = {}) {
42
43
  (0, logger_1.logStep)('Organising generated DTO files...');
43
44
  const sourceDir = path_1.default.join(tempDir, 'model');
44
45
  const destDir = path_1.default.join(outputDir, 'data/dtos');
@@ -47,8 +48,13 @@ function organizeFiles(tempDir, outputDir) {
47
48
  fs_extra_1.default.ensureDirSync(destDir);
48
49
  const files = fs_extra_1.default.readdirSync(sourceDir).filter((file) => file.endsWith('.dto.ts'));
49
50
  files.forEach((file) => {
51
+ // file is like "userResponse.dto.ts" → derive PascalCase schema name to look up tag
52
+ const camelName = file.replace('.dto.ts', '');
53
+ const pascalName = (0, name_formatter_1.toPascalCase)(camelName);
54
+ const tagFolder = schemaTagMap[pascalName] || 'shared';
50
55
  const sourcePath = path_1.default.join(sourceDir, file);
51
- const destPath = path_1.default.join(destDir, file);
56
+ const destPath = path_1.default.join(destDir, tagFolder, file);
57
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
52
58
  fs_extra_1.default.copySync(sourcePath, destPath);
53
59
  filesMoved++;
54
60
  (0, logger_1.logDetail)('dto', `${file} → ${path_1.default.relative(process.cwd(), destPath)}`);
@@ -62,46 +68,60 @@ function addDtoImports(outputDir) {
62
68
  const dtosDir = path_1.default.join(outputDir, 'data/dtos');
63
69
  if (!fs_extra_1.default.existsSync(dtosDir))
64
70
  return;
65
- const files = fs_extra_1.default.readdirSync(dtosDir).filter((f) => f.endsWith('.dto.ts'));
66
- // Build a map of DTO classname → file base name (without .ts)
71
+ // Collect all .dto.ts files from all subfolders (1 level deep)
72
+ const allFiles = [];
73
+ const entries = fs_extra_1.default.readdirSync(dtosDir);
74
+ entries.forEach((entry) => {
75
+ const entryPath = path_1.default.join(dtosDir, entry);
76
+ if (fs_extra_1.default.statSync(entryPath).isDirectory()) {
77
+ fs_extra_1.default.readdirSync(entryPath)
78
+ .filter((f) => f.endsWith('.dto.ts'))
79
+ .forEach((file) => allFiles.push({ subfolder: entry, file, fullPath: path_1.default.join(entryPath, file) }));
80
+ }
81
+ else if (entry.endsWith('.dto.ts')) {
82
+ allFiles.push({ subfolder: '', file: entry, fullPath: entryPath });
83
+ }
84
+ });
85
+ // Build map: ClassName → { subfolder, fileBase }
67
86
  const dtoMap = {};
68
- files.forEach((file) => {
69
- const content = fs_extra_1.default.readFileSync(path_1.default.join(dtosDir, file), 'utf8');
87
+ allFiles.forEach(({ subfolder, file, fullPath }) => {
88
+ const content = fs_extra_1.default.readFileSync(fullPath, 'utf8');
70
89
  const match = content.match(/export interface (\w+)/);
71
- if (match) {
72
- dtoMap[match[1]] = file.replace('.ts', '');
73
- }
90
+ if (match)
91
+ dtoMap[match[1]] = { subfolder, fileBase: file.replace('.ts', '') };
74
92
  });
75
93
  let filesProcessed = 0;
76
- files.forEach((file) => {
77
- const filePath = path_1.default.join(dtosDir, file);
78
- const originalContent = fs_extra_1.default.readFileSync(filePath, 'utf8');
94
+ allFiles.forEach(({ subfolder, file, fullPath }) => {
95
+ const originalContent = fs_extra_1.default.readFileSync(fullPath, 'utf8');
79
96
  let content = originalContent;
80
97
  const selfMatch = content.match(/export interface (\w+)/);
81
98
  const selfName = selfMatch ? selfMatch[1] : '';
82
- // Normalize Array<T> → T[] (openapi-generator-cli always outputs Array<T>)
83
99
  content = content.replace(/Array<(\w+)>/g, '$1[]');
84
- // Find all Dto type references in the file body (excluding the interface name itself)
85
100
  const references = new Set();
86
101
  const typeRegex = /\b(\w+Dto)\b/g;
87
102
  let match;
88
103
  while ((match = typeRegex.exec(content)) !== null) {
89
- if (match[1] !== selfName) {
104
+ if (match[1] !== selfName)
90
105
  references.add(match[1]);
91
- }
92
106
  }
93
- // Build import lines for each referenced type that exists in the dtoMap
94
107
  const imports = [];
95
108
  references.forEach((ref) => {
96
109
  if (dtoMap[ref]) {
97
- imports.push(`import { ${ref} } from './${dtoMap[ref]}';`);
110
+ const { subfolder: refSubfolder, fileBase: refFileBase } = dtoMap[ref];
111
+ const fromDir = subfolder ? path_1.default.join(dtosDir, subfolder) : dtosDir;
112
+ const toFile = refSubfolder
113
+ ? path_1.default.join(dtosDir, refSubfolder, refFileBase)
114
+ : path_1.default.join(dtosDir, refFileBase);
115
+ let relPath = path_1.default.relative(fromDir, toFile).replace(/\\/g, '/');
116
+ if (!relPath.startsWith('.'))
117
+ relPath = './' + relPath;
118
+ imports.push(`import { ${ref} } from '${relPath}';`);
98
119
  }
99
120
  });
100
- if (imports.length > 0) {
121
+ if (imports.length > 0)
101
122
  content = imports.join('\n') + '\n' + content;
102
- }
103
123
  if (content !== originalContent) {
104
- fs_extra_1.default.writeFileSync(filePath, content);
124
+ fs_extra_1.default.writeFileSync(fullPath, content);
105
125
  filesProcessed++;
106
126
  (0, logger_1.logDetail)('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
107
127
  }
@@ -7,6 +7,7 @@ exports.generateReport = generateReport;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const logger_1 = require("../utils/logger");
10
+ const example_validator_1 = require("../utils/example-validator");
10
11
  /** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
11
12
  function countMockFiles(dir) {
12
13
  try {
@@ -37,6 +38,7 @@ function generateReport(outputDir, analysis, lintResult) {
37
38
  })).length;
38
39
  return { name: t.name, description: t.description || '', endpoints: endpointCount };
39
40
  });
41
+ const exampleMismatches = (0, example_validator_1.getExampleMismatches)();
40
42
  const report = {
41
43
  timestamp: new Date().toISOString(),
42
44
  tags: analysis.tags.length,
@@ -44,6 +46,10 @@ function generateReport(outputDir, analysis, lintResult) {
44
46
  tagDetails,
45
47
  outputDirectory: outputDir,
46
48
  linting: lintResult,
49
+ warnings: {
50
+ exampleMismatches: exampleMismatches.map((m) => ({ ...m })),
51
+ total: exampleMismatches.length
52
+ },
47
53
  structure: {
48
54
  dtos: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/dtos')).length,
49
55
  repositories: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/repositories')).length,
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Validates that OpenAPI `example` values match their declared `type`.
4
+ *
5
+ * YAML parses unquoted values by native type (e.g. `example: 68131` becomes a JS number
6
+ * even when the schema declares `type: string`). This module detects such mismatches,
7
+ * coerces them when possible, and accumulates warnings for the generation report.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.validateExample = validateExample;
11
+ exports.registerMismatch = registerMismatch;
12
+ exports.getExampleMismatches = getExampleMismatches;
13
+ exports.clearExampleMismatches = clearExampleMismatches;
14
+ // Module-level accumulator — reset between runs via `clearExampleMismatches()`.
15
+ let mismatches = [];
16
+ /**
17
+ * Validates an `example` value against a declared OpenAPI `type`.
18
+ *
19
+ * @returns `{ valid: true }` when types already match, or
20
+ * `{ valid: false, coerced: <value> }` when the value was coerced, or
21
+ * `{ valid: false }` when coercion is not possible (caller should ignore the example).
22
+ */
23
+ function validateExample(declaredType, example) {
24
+ if (declaredType === undefined)
25
+ return { valid: true };
26
+ const jsType = typeof example;
27
+ // ── string declared ──────────────────────────────────────────────────────
28
+ if (declaredType === 'string') {
29
+ if (jsType === 'string')
30
+ return { valid: true };
31
+ // number or boolean → coerce to string
32
+ if (jsType === 'number' || jsType === 'boolean') {
33
+ return { valid: false, coerced: String(example) };
34
+ }
35
+ return { valid: false };
36
+ }
37
+ // ── integer / number declared ────────────────────────────────────────────
38
+ if (declaredType === 'integer' || declaredType === 'number') {
39
+ if (jsType === 'number')
40
+ return { valid: true };
41
+ if (jsType === 'string') {
42
+ const parsed = Number(example);
43
+ if (!Number.isNaN(parsed))
44
+ return { valid: false, coerced: parsed };
45
+ return { valid: false }; // unparseable → ignore
46
+ }
47
+ return { valid: false };
48
+ }
49
+ // ── boolean declared ─────────────────────────────────────────────────────
50
+ if (declaredType === 'boolean') {
51
+ if (jsType === 'boolean')
52
+ return { valid: true };
53
+ if (jsType === 'string') {
54
+ const lower = example.toLowerCase();
55
+ if (lower === 'true')
56
+ return { valid: false, coerced: true };
57
+ if (lower === 'false')
58
+ return { valid: false, coerced: false };
59
+ }
60
+ return { valid: false }; // cannot coerce
61
+ }
62
+ // Other types (object, array, etc.) — no validation
63
+ return { valid: true };
64
+ }
65
+ /**
66
+ * Records a mismatch so it can be retrieved later for console warnings and the report.
67
+ */
68
+ function registerMismatch(schemaName, propertyName, declaredType, exampleValue, action, coercedValue) {
69
+ mismatches.push({
70
+ schemaName,
71
+ propertyName,
72
+ declaredType,
73
+ exampleValue,
74
+ exampleJsType: typeof exampleValue,
75
+ action,
76
+ coercedValue
77
+ });
78
+ }
79
+ /** Returns all recorded mismatches. */
80
+ function getExampleMismatches() {
81
+ return mismatches;
82
+ }
83
+ /** Clears all recorded mismatches (call before each generation run). */
84
+ function clearExampleMismatches() {
85
+ mismatches = [];
86
+ }
@@ -1,17 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveMockValue = resolveMockValue;
4
+ const example_validator_1 = require("./example-validator");
4
5
  /**
5
6
  * Resolves a TypeScript literal string to use as a mock value for a single schema property.
6
7
  *
7
8
  * Priority chain:
8
9
  * $ref mock call → array $ref mock call → enum[0] → example → format fallback → type default
9
10
  *
10
- * @param propName Property name (used for format heuristics such as "email").
11
- * @param prop Raw OpenAPI property definition.
12
- * @param context 'dto' generates `mockFooDto()`, 'model' generates `mockFooModel()`.
11
+ * @param propName Property name (used for format heuristics such as "email").
12
+ * @param prop Raw OpenAPI property definition.
13
+ * @param context 'dto' generates `mockFooDto()`, 'model' generates `mockFooModel()`.
14
+ * @param schemaName Parent schema name (used for mismatch reporting).
13
15
  */
14
- function resolveMockValue(propName, prop, context = 'dto') {
16
+ function resolveMockValue(propName, prop, context = 'dto', schemaName = 'unknown') {
15
17
  const suffix = context === 'dto' ? 'Dto' : 'Model';
16
18
  // 1. Direct $ref → call the referenced mock factory
17
19
  if (prop.$ref) {
@@ -31,9 +33,19 @@ function resolveMockValue(propName, prop, context = 'dto') {
31
33
  const first = prop.enum[0];
32
34
  return typeof first === 'string' ? `'${first}'` : String(first);
33
35
  }
34
- // 5. Example value from the swagger spec (highest fidelity)
35
- if (prop.example !== undefined)
36
- return formatLiteral(prop.example);
36
+ // 5. Example value validated and coerced if needed
37
+ if (prop.example !== undefined) {
38
+ const result = (0, example_validator_1.validateExample)(prop.type, prop.example);
39
+ if (result.valid) {
40
+ return formatLiteral(prop.example);
41
+ }
42
+ if (result.coerced !== undefined) {
43
+ (0, example_validator_1.registerMismatch)(schemaName, propName, prop.type, prop.example, 'coerced', result.coerced);
44
+ return formatLiteral(result.coerced);
45
+ }
46
+ // Cannot coerce — register and fall through to defaults
47
+ (0, example_validator_1.registerMismatch)(schemaName, propName, prop.type, prop.example, 'ignored');
48
+ }
37
49
  // 6. Format-aware fallbacks (when no example is provided)
38
50
  if (prop.format === 'date-time')
39
51
  return `'2024-01-01T00:00:00.000Z'`;
@@ -1,16 +1,113 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toPascalCase = toPascalCase;
3
4
  exports.toCamelCase = toCamelCase;
5
+ exports.isReservedWord = isReservedWord;
6
+ exports.safePropertyName = safePropertyName;
4
7
  /**
5
- * Converts a PascalCase name to camelCase by lowercasing the first character.
8
+ * Converts a string to PascalCase, handling spaces, hyphens and underscores.
9
+ * Used to derive class names from schema/tag names.
10
+ *
11
+ * @example
12
+ * toPascalCase('Product Format') // 'ProductFormat'
13
+ * toPascalCase('user-response') // 'UserResponse'
14
+ * toPascalCase('UserSchema') // 'UserSchema'
15
+ */
16
+ function toPascalCase(name) {
17
+ if (!name)
18
+ return name;
19
+ return name
20
+ .split(/[\s\-_]+/)
21
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
22
+ .join('');
23
+ }
24
+ /**
25
+ * Converts a string to camelCase, handling spaces, hyphens and underscores.
6
26
  * Used to derive class filenames and variable names from schema/tag names.
7
27
  *
8
28
  * @example
29
+ * toCamelCase('Product Format') // 'productFormat'
9
30
  * toCamelCase('ProductResponse') // 'productResponse'
10
31
  * toCamelCase('UserSchema') // 'userSchema'
11
32
  */
12
33
  function toCamelCase(name) {
13
34
  if (!name)
14
35
  return name;
15
- return name.charAt(0).toLowerCase() + name.slice(1);
36
+ const pascal = toPascalCase(name);
37
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
38
+ }
39
+ const JS_RESERVED_WORDS = new Set([
40
+ 'abstract',
41
+ 'arguments',
42
+ 'await',
43
+ 'boolean',
44
+ 'break',
45
+ 'byte',
46
+ 'case',
47
+ 'catch',
48
+ 'char',
49
+ 'class',
50
+ 'const',
51
+ 'continue',
52
+ 'debugger',
53
+ 'default',
54
+ 'delete',
55
+ 'do',
56
+ 'double',
57
+ 'else',
58
+ 'enum',
59
+ 'eval',
60
+ 'export',
61
+ 'extends',
62
+ 'false',
63
+ 'final',
64
+ 'finally',
65
+ 'float',
66
+ 'for',
67
+ 'function',
68
+ 'goto',
69
+ 'if',
70
+ 'implements',
71
+ 'import',
72
+ 'in',
73
+ 'instanceof',
74
+ 'int',
75
+ 'interface',
76
+ 'let',
77
+ 'long',
78
+ 'native',
79
+ 'new',
80
+ 'null',
81
+ 'package',
82
+ 'private',
83
+ 'protected',
84
+ 'public',
85
+ 'return',
86
+ 'short',
87
+ 'static',
88
+ 'super',
89
+ 'switch',
90
+ 'synchronized',
91
+ 'this',
92
+ 'throw',
93
+ 'throws',
94
+ 'transient',
95
+ 'true',
96
+ 'try',
97
+ 'typeof',
98
+ 'undefined',
99
+ 'var',
100
+ 'void',
101
+ 'volatile',
102
+ 'while',
103
+ 'with',
104
+ 'yield'
105
+ ]);
106
+ /** Returns true if the given name is a JS/TS reserved word. */
107
+ function isReservedWord(name) {
108
+ return JS_RESERVED_WORDS.has(name);
109
+ }
110
+ /** Prefixes reserved words with `_` to produce a safe identifier. */
111
+ function safePropertyName(name) {
112
+ return isReservedWord(name) ? `_${name}` : name;
16
113
  }
@@ -4,7 +4,7 @@
4
4
  import { InjectionToken } from '@angular/core';
5
5
  import { Observable } from 'rxjs';
6
6
  {{#imports}}
7
- import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
7
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
8
8
  {{/imports}}
9
9
 
10
10
  /**
@@ -4,9 +4,9 @@
4
4
  import { MockService } from 'ng-mocks';
5
5
  import { of } from 'rxjs';
6
6
 
7
- import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
7
+ import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl';
8
8
  {{#returnImports}}
9
- import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
9
+ import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
10
10
  {{/returnImports}}
11
11
 
12
12
  export const mock{{classname}}RepositoryImpl = () =>
@@ -9,14 +9,14 @@ import { environment } from '@environment';
9
9
 
10
10
  import { MRepository } from '@mercadona/core/utils/repository';
11
11
 
12
- import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
12
+ import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
13
13
  {{#returnImports}}
14
- import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
15
- import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
16
- import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
14
+ import { {{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto';
15
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
16
+ import { {{classVarName}}Mapper } from '@/mappers/{{tagFilename}}/{{classFilename}}.mapper';
17
17
  {{/returnImports}}
18
18
  {{#paramImports}}
19
- import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
19
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
20
20
  {{/paramImports}}
21
21
 
22
22
  /**
@@ -6,8 +6,8 @@ import { TestBed } from '@angular/core/testing';
6
6
 
7
7
  import { {{classname}}RepositoryImpl } from './{{classFilename}}.repository.impl';
8
8
  {{#returnImports}}
9
- import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
10
- import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
9
+ import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
10
+ import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
11
11
  {{/returnImports}}
12
12
 
13
13
  describe('{{classname}}RepositoryImpl', () => {
@@ -4,7 +4,7 @@
4
4
  import { InjectionToken } from '@angular/core';
5
5
  import { Observable } from 'rxjs';
6
6
  {{#imports}}
7
- import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
7
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
8
8
  {{/imports}}
9
9
 
10
10
  /**
@@ -6,9 +6,9 @@ import { Observable } from 'rxjs';
6
6
 
7
7
  import { {{classname}}UseCases } from './{{classFilename}}.use-cases.contract';
8
8
 
9
- import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
9
+ import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
10
10
  {{#imports}}
11
- import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
11
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
12
12
  {{/imports}}
13
13
 
14
14
  /**
@@ -6,9 +6,9 @@ import { of } from 'rxjs';
6
6
 
7
7
  import { {{classname}}UseCasesImpl } from './{{classFilename}}.use-cases.impl';
8
8
 
9
- import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
9
+ import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
10
10
  {{#returnImports}}
11
- import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
11
+ import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
12
12
  {{/returnImports}}
13
13
 
14
14
  describe('{{classname}}UseCasesImpl', () => {
@@ -4,9 +4,9 @@
4
4
  import { MockService } from 'ng-mocks';
5
5
  import { of } from 'rxjs';
6
6
 
7
- import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
7
+ import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
8
8
  {{#returnImports}}
9
- import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
9
+ import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
10
10
  {{/returnImports}}
11
11
 
12
12
  export const mock{{classname}}UseCasesImpl = () =>
@@ -1,7 +1,7 @@
1
1
  {{#models}}
2
2
  {{#model}}
3
3
  {{#mockImports}}
4
- import { mock{{classname}}Dto } from './{{classFilename}}.dto.mock';
4
+ import { mock{{classname}}Dto } from '{{{importPath}}}';
5
5
  {{/mockImports}}
6
6
  import { {{classname}}Dto } from './{{classFilename}}.dto';
7
7
 
@@ -4,8 +4,8 @@
4
4
  import { MapFromFn } from '@mercadona/common/public';
5
5
  import { Builder } from '@mercadona/common/utils';
6
6
 
7
- import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
8
- import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
7
+ import { {{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto';
8
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
9
9
 
10
10
  /**
11
11
  * {{classname}} Mapper
@@ -2,8 +2,8 @@
2
2
  {{#model}}
3
3
  import { {{classVarName}}Mapper } from './{{classFilename}}.mapper';
4
4
 
5
- import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
6
- import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
5
+ import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
6
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
7
7
 
8
8
  describe('{{classVarName}}Mapper', () => {
9
9
  {{#vars}}
@@ -1,7 +1,7 @@
1
1
  {{#models}}
2
2
  {{#model}}
3
3
  {{#imports}}
4
- import { {{classname}} } from './{{classFilename}}.model';
4
+ import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
5
5
  {{/imports}}
6
6
 
7
7
  /**
@@ -11,6 +11,7 @@ describe('{{classname}}', () => {
11
11
  });
12
12
 
13
13
  {{#vars}}
14
+ {{#hasMockValue}}
14
15
  it('should allow setting {{name}}', () => {
15
16
  const model = new {{classname}}();
16
17
  const expected = mock{{classname}}Model();
@@ -19,13 +20,16 @@ describe('{{classname}}', () => {
19
20
  expect(model.{{name}}).toBe(expected.{{name}});
20
21
  });
21
22
 
23
+ {{/hasMockValue}}
22
24
  {{/vars}}
23
25
  it('should build a valid model from mock', () => {
24
26
  const model = mock{{classname}}Model();
25
27
 
26
28
  expect(model).toBeInstanceOf({{classname}});
27
29
  {{#vars}}
30
+ {{#hasMockValue}}
28
31
  expect(model.{{name}}).toBeDefined();
32
+ {{/hasMockValue}}
29
33
  {{/vars}}
30
34
  });
31
35
  });
@@ -1,8 +1,8 @@
1
1
  {{#models}}
2
2
  {{#model}}
3
3
  import { {{classname}} } from './{{classFilename}}.model';
4
- import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
5
- import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
4
+ import { {{classVarName}}Mapper } from '@/mappers/{{tagFilename}}/{{classFilename}}.mapper';
5
+ import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
6
6
 
7
7
  export const mock{{classname}}Model = (overrides: Partial<{{classname}}> = {}): {{classname}} =>
8
8
  Object.assign(new {{classname}}(), {
@@ -3,8 +3,8 @@
3
3
  {{#operations}}
4
4
  import { Provider } from '@angular/core';
5
5
 
6
- import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract';
7
- import { mock{{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl.mock';
6
+ import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
7
+ import { mock{{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl.mock';
8
8
 
9
9
  export function mock{{classname}}Repository(): Provider[] {
10
10
  return [
@@ -3,8 +3,8 @@
3
3
  {{#operations}}
4
4
  import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
5
5
 
6
- import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract';
7
- import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
6
+ import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
7
+ import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl';
8
8
 
9
9
  /**
10
10
  * {{classname}} Repository Provider
@@ -3,8 +3,8 @@
3
3
  {{#operations}}
4
4
  import { Provider } from '@angular/core';
5
5
 
6
- import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
7
- import { mock{{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.mock';
6
+ import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
7
+ import { mock{{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.mock';
8
8
 
9
9
  export function mock{{classname}}UseCases(): Provider[] {
10
10
  return [
@@ -3,8 +3,8 @@
3
3
  {{#operations}}
4
4
  import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
5
5
 
6
- import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
7
- import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
6
+ import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
7
+ import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
8
8
 
9
9
  /**
10
10
  * {{classname}} Use Cases Provider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0kmpo/openapi-clean-arch-generator",
3
- "version": "1.3.14",
3
+ "version": "1.3.15",
4
4
  "description": "Angular Clean Architecture generator from OpenAPI/Swagger",
5
5
  "main": "dist/main.js",
6
6
  "bin": {