21 Commits

Author SHA1 Message Date
da295c78ba Merge branch 'main' into fix/name-generator
All checks were successful
Lint / lint (pull_request) Successful in 36s
2026-03-27 15:41:05 +01:00
16ef1ce3e3 feat: enhance DTO mock generation with dynamic import paths and mock value checks 2026-03-27 15:40:33 +01:00
e0446d4939 feat: add example validation and mismatch reporting for OpenAPI schemas 2026-03-27 15:27:03 +01:00
902c0fd5a2 Merge pull request 'chore: update README with new CLI options for configuration and linting & add NOTICE' (#66) from feat/update-docu into main
Reviewed-on: #66
Reviewed-by: blas <me@blassanto.me>
2026-03-27 14:11:28 +00:00
ef804b74c2 Merge branch 'main' into feat/update-docu
All checks were successful
Lint / lint (pull_request) Successful in 13s
2026-03-27 14:11:12 +00:00
98cd7a5edb Merge pull request 'chore: fix releases to npm' (#70) from fix/dist-result into main
Reviewed-on: #70
Reviewed-by: didavila <diego.davilafreitas@gmail.com>
2026-03-27 14:10:45 +00:00
3ede53ae3b chore: bump to version v1.3.14 2026-03-27 13:54:17 +00:00
aa7c6cf338 chore: add dist to package.json
All checks were successful
Publish / publish (push) Successful in 1m47s
Lint / lint (pull_request) Successful in 13s
2026-03-27 14:53:39 +01:00
0075717a1f chore: bump to version v1.3.13 2026-03-27 13:47:08 +00:00
aab9bf01bb feat: sort parameters in buildTagsMapFromAnalysis by required status 2026-03-27 14:46:06 +01:00
7c5af2f3ab chore: add dist to package.json
All checks were successful
Publish / publish (push) Successful in 1m44s
2026-03-27 14:45:53 +01:00
e667c7bf04 chore: bump to version v1.3.12 2026-03-27 13:45:13 +00:00
2e9aecdffe chore: bump to version v1.3.11 2026-03-27 13:38:06 +00:00
2db6e95b1c feat: enhance DTO generation and organization by tag 2026-03-27 14:34:30 +01:00
5304ed7047 chore: bump to version v0.1 2026-03-27 13:30:51 +00:00
56bd6f4e9f chore: bump to version v0.1-test 2026-03-27 13:28:49 +00:00
603feda26d feat: enhance name formatting functions to include PascalCase conversion 2026-03-27 14:09:30 +01:00
3eb604c95f chore: update copyright year in NOTICE file to 2026
All checks were successful
Lint / lint (pull_request) Successful in 13s
2026-03-27 12:15:36 +01:00
4293857a7d chore: add NOTICE file with third-party licenses and copyright information
All checks were successful
Lint / lint (pull_request) Successful in 13s
2026-03-27 11:35:23 +01:00
07e4559133 chore: update README with new command options for skipping linting and using config files
All checks were successful
Lint / lint (pull_request) Successful in 13s
2026-03-27 11:29:52 +01:00
bdd0e4873e chore: update README with new CLI options for configuration and linting
All checks were successful
Lint / lint (pull_request) Successful in 39s
2026-03-27 11:26:54 +01:00
29 changed files with 778 additions and 269 deletions

50
NOTICE Normal file
View File

@@ -0,0 +1,50 @@
OpenAPI Clean Architecture Generator
Copyright (c) 2026 Blas Santomé Ocampo
This product includes software developed by third parties.
All third-party packages listed below are distributed under the MIT License.
---
chalk v4.1.2
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
License: MIT
https://github.com/chalk/chalk
---
commander v11.1.0
Copyright (c) TJ Holowaychuk <tj@vision-media.ca>
License: MIT
https://github.com/tj/commander.js
---
fs-extra v11.3.4
Copyright (c) JP Richardson <jprichardson@gmail.com>
License: MIT
https://github.com/jprichardson/node-fs-extra
---
js-yaml v4.1.1
Copyright (c) Vladimir Zapparov <dervus.grim@gmail.com>
Copyright (c) Aleksey V Zapparov <ixti@member.fsf.org>
Copyright (c) Vitaly Puzrin <vitaly@rcdesign.ru>
Copyright (c) Martin Grenfell <martin.grenfell@gmail.com>
License: MIT
https://github.com/nodeca/js-yaml
---
mustache v4.2.0
Copyright (c) mustache.js Authors (http://github.com/janl/mustache.js)
License: MIT
https://github.com/janl/mustache.js
---
prompts v2.4.2
Copyright (c) Terkel Gjervig
License: MIT
https://github.com/terkelg/prompts

View File

@@ -76,7 +76,10 @@ Options:
-o, --output <dir> Output directory [default: ./src/app]
-t, --templates <dir> Custom templates directory [default: ./templates]
-s, --select-endpoints Interactively select tags and endpoints to generate
-c, --config <file> Use a JSON configuration file (skips interactive prompts)
--init-config [file] Generate a JSON configuration file instead of generating code
--skip-install Skip dependency installation
--skip-lint Skip post-generation linting and formatting
--dry-run Simulate without writing files
-h, --help Show help
```
@@ -96,6 +99,15 @@ generate-clean-arch -i api.yaml -t ./my-templates
# Dry run (no files written)
generate-clean-arch -i swagger.yaml --dry-run
# Skip linting after generation
generate-clean-arch -i swagger.yaml --skip-lint
# Generate a config file to reuse later
generate-clean-arch --init-config generation-config.json
# Run using a config file (no interactive prompts)
generate-clean-arch -c generation-config.json
# Full example with all options
generate-clean-arch -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates
```

45
main.ts
View File

@@ -20,11 +20,14 @@ import { analyzeSwagger } from './src/swagger/analyzer';
import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator';
import {
generateCleanArchitecture,
extractTagsWithOperations
extractTagsWithOperations,
buildTagsMapFromAnalysis,
buildSchemaTagMap
} from './src/generators/clean-arch.generator';
import { generateReport } from './src/generators/report.generator';
import { lintGeneratedFiles } from './src/generators/lint.generator';
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
import { getExampleMismatches, clearExampleMismatches } from './src/utils/example-validator';
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
import {
loadConfig,
@@ -144,6 +147,7 @@ async function main(): Promise<void> {
}
createDirectoryStructure(options.output);
clearExampleMismatches();
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
let selectionFilter: SelectionFilter = {};
@@ -193,17 +197,47 @@ async function main(): Promise<void> {
// ──────────────────────────────────────────────────────────────────────────
const tempDir = generateCode(options.input, options.templates);
organizeFiles(tempDir, options.output);
// Compute schema→tag map before organizeFiles so DTOs land in the right subfolder
const tagsMapForSchema = buildTagsMapFromAnalysis(analysis, selectionFilter);
const schemaTagMap = buildSchemaTagMap(
(analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
?.schemas || {},
tagsMapForSchema
);
organizeFiles(tempDir, options.output, schemaTagMap);
addDtoImports(options.output);
generateCleanArchitecture(
analysis,
options.output,
options.templates,
tagApiKeyMap,
selectionFilter
selectionFilter,
schemaTagMap
);
cleanup(tempDir);
// ── EXAMPLE/TYPE MISMATCH WARNINGS ─────────────────────────────────────────
const mismatches = getExampleMismatches();
if (mismatches.length > 0) {
console.log('');
logWarning(`${mismatches.length} example/type mismatch(es) detected in OpenAPI schemas:`);
for (const m of mismatches) {
const action =
m.action === 'coerced'
? `→ coerced to ${JSON.stringify(m.coercedValue)}`
: '→ example ignored, using type default';
logWarning(
` ${m.schemaName}.${m.propertyName}: type '${m.declaredType}' but example is ${m.exampleJsType} (${JSON.stringify(m.exampleValue)}) ${action}`
);
logDetail(
'VALIDATE',
`${m.schemaName}.${m.propertyName}: declared=${m.declaredType} example=${JSON.stringify(m.exampleValue)} (${m.exampleJsType}) action=${m.action}`
);
}
}
const noLintResult: LintResult = {
prettier: { ran: false, filesFormatted: 0 },
eslint: { ran: false, filesFixed: 0 }
@@ -222,6 +256,11 @@ async function main(): Promise<void> {
console.log(` - Use Cases: ${report.structure.useCases}`);
console.log(` - Providers: ${report.structure.providers}`);
console.log(` - Mocks: ${report.structure.mocks}`);
if (report.warnings.total > 0) {
console.log(
`\n ${colors.yellow}⚠️ ${report.warnings.total} example/type mismatch(es) (see above)${colors.reset}`
);
}
console.log(`\n📁 Files generated in: ${colors.cyan}${options.output}${colors.reset}\n`);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@0kmpo/openapi-clean-arch-generator",
"version": "1.3.10",
"version": "1.3.14",
"description": "Angular Clean Architecture generator from OpenAPI/Swagger",
"main": "dist/main.js",
"bin": {
@@ -40,7 +40,10 @@
],
"license": "MIT",
"files": [
"dist/",
"dist/main.js",
"dist/package.json",
"dist/src/",
"dist/templates/",
"README.md",
"LICENSE"
],

View File

@@ -3,7 +3,7 @@ import path from 'path';
import mustache from 'mustache';
import { logStep, logSuccess, logDetail } from '../utils/logger';
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
import { toCamelCase } from '../utils/name-formatter';
import { toCamelCase, toPascalCase, safePropertyName } from '../utils/name-formatter';
import { resolveMockValue } from '../utils/mock-value-resolver';
import type {
SwaggerAnalysis,
@@ -59,181 +59,14 @@ export function extractTagsWithOperations(analysis: SwaggerAnalysis): TagSummary
return [...map.values()];
}
/** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
export function generateCleanArchitecture(
/**
* Builds and returns the tagsMap from the swagger analysis, applying the optional selection filter.
* Exported so callers (e.g. main.ts) can compute it before organizeFiles runs.
*/
export function buildTagsMapFromAnalysis(
analysis: SwaggerAnalysis,
outputDir: string,
templatesDir: string,
tagApiKeyMap: Record<string, string> = {},
selectionFilter: SelectionFilter = {}
): GeneratedCount {
logStep('Generating Clean Architecture artefacts using Mustache...');
const generatedCount: GeneratedCount = {
models: 0,
repositories: 0,
mappers: 0,
useCases: 0,
providers: 0,
mocks: 0,
specs: 0
};
const schemas =
(analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
?.schemas || {};
// 1. Generate Models, Entities and Mappers from Schemas
Object.keys(schemas).forEach((schemaName) => {
const baseName = schemaName.replace(/Dto$/, '');
const schemaObj = schemas[schemaName] as OpenApiSchema;
const rawProperties = schemaObj.properties || {};
const requiredProps: string[] = schemaObj.required || [];
const varsMap = Object.keys(rawProperties).map((k) => {
let tsType = mapSwaggerTypeToTs(rawProperties[k].type);
if (rawProperties[k].$ref) {
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
}
return {
name: k,
dataType: tsType,
description: rawProperties[k].description || '',
required: requiredProps.includes(k)
};
});
// Collect imports for types referenced via $ref in properties
const referencedTypes = new Set<string>();
Object.values(rawProperties).forEach((prop) => {
if (prop.$ref) {
referencedTypes.add(prop.$ref.split('/').pop() || '');
} else if (prop.type === 'array' && prop.items?.$ref) {
referencedTypes.add(prop.items.$ref.split('/').pop() || '');
}
});
const modelImports = [...referencedTypes]
.filter(Boolean)
.map((name) => ({ classname: name, classFilename: toCamelCase(name) }));
const modelViewData = {
models: [
{
model: {
classname: baseName,
classFilename: toCamelCase(baseName),
classVarName: toCamelCase(baseName),
description: schemaObj.description || '',
imports: modelImports,
vars: varsMap
}
}
],
allModels: [{ model: { vars: varsMap } }]
};
const mapperViewData = {
...modelViewData,
apiInfo: {
apis: [
{
operations: {
classname: baseName,
classFilename: toCamelCase(baseName),
classVarName: toCamelCase(baseName)
}
}
]
}
};
// Model (Entities)
const modelTemplatePath = path.join(templatesDir, 'model-entity.mustache');
if (fs.existsSync(modelTemplatePath)) {
const template = fs.readFileSync(modelTemplatePath, 'utf8');
const output = mustache.render(template, modelViewData);
const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`);
fs.writeFileSync(destPath, output);
generatedCount.models++;
logDetail('generate', `model-entity → ${path.relative(process.cwd(), destPath)}`);
}
// Mapper
const mapperTemplatePath = path.join(templatesDir, 'mapper.mustache');
if (fs.existsSync(mapperTemplatePath)) {
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
const output = mustache.render(template, mapperViewData);
const destPath = path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.ts`);
fs.writeFileSync(destPath, output);
generatedCount.mappers++;
}
// DTO mock — values resolved from raw schema (example, format, type)
const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
name: k,
mockValue: resolveMockValue(k, rawProperties[k], 'dto')
}));
const dtoMockImports = [...referencedTypes]
.filter(Boolean)
.map((name) => ({ classname: name, classFilename: toCamelCase(name) }));
const dtoMockViewData = {
models: [
{
model: {
classname: baseName,
classFilename: toCamelCase(baseName),
classVarName: toCamelCase(baseName),
mockImports: dtoMockImports,
vars: dtoMockVarsMap
}
}
]
};
renderTemplate(
templatesDir,
'dto.mock.mustache',
dtoMockViewData,
path.join(outputDir, 'data/dtos', `${toCamelCase(baseName)}.dto.mock.ts`),
generatedCount,
'mocks'
);
// Model mock — delegates to mapper + DTO mock (no property values needed)
renderTemplate(
templatesDir,
'model.mock.mustache',
modelViewData,
path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.mock.ts`),
generatedCount,
'mocks'
);
// Model spec
renderTemplate(
templatesDir,
'model-entity.spec.mustache',
modelViewData,
path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.spec.ts`),
generatedCount,
'specs'
);
// Mapper spec
renderTemplate(
templatesDir,
'mapper.spec.mustache',
mapperViewData,
path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.spec.ts`),
generatedCount,
'specs'
);
});
// 2. Generate Use Cases and Repositories from Paths/Tags
): Record<string, TagOperation[]> {
const tagsMap: Record<string, TagOperation[]> = {};
Object.keys(analysis.paths).forEach((pathKey) => {
@@ -290,6 +123,12 @@ export function generateCleanArchitecture(
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
const hasBodyParam = !!op.requestBody;
// Sort: required params first, optional params last (TypeScript requirement)
allParams.sort((a, b) => {
if (a.required === b.required) return 0;
return a.required ? -1 : 1;
});
tagsMap[tag].push({
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
summary: op.summary || '',
@@ -334,10 +173,276 @@ export function generateCleanArchitecture(
});
}
return tagsMap;
}
/**
* Maps each schema basename to the tag subfolder it belongs to.
* Schemas used by exactly one tag → that tag's camelCase name.
* Schemas used by 0 or multiple tags → 'shared'.
*/
export function buildSchemaTagMap(
schemas: Record<string, unknown>,
tagsMap: Record<string, TagOperation[]>
): Record<string, string> {
const result: Record<string, string> = {};
Object.keys(schemas).forEach((schemaName) => {
const baseName = schemaName.replace(/Dto$/, '');
const tagsUsing: string[] = [];
Object.keys(tagsMap).forEach((tag) => {
const used = tagsMap[tag].some(
(op) =>
op.returnType === baseName ||
op.returnType === `${baseName}[]` ||
op.allParams.some((p) => p.dataType === baseName || p.dataType === `${baseName}[]`)
);
if (used) tagsUsing.push(tag);
});
result[baseName] = tagsUsing.length === 1 ? toCamelCase(tagsUsing[0]) : 'shared';
});
return result;
}
/** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
export function generateCleanArchitecture(
analysis: SwaggerAnalysis,
outputDir: string,
templatesDir: string,
tagApiKeyMap: Record<string, string> = {},
selectionFilter: SelectionFilter = {},
precomputedSchemaTagMap: Record<string, string> = {}
): GeneratedCount {
logStep('Generating Clean Architecture artefacts using Mustache...');
const generatedCount: GeneratedCount = {
models: 0,
repositories: 0,
mappers: 0,
useCases: 0,
providers: 0,
mocks: 0,
specs: 0
};
const schemas =
(analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
?.schemas || {};
// Build tagsMap first — needed to compute schemaTagMap before the schema loop
const tagsMap = buildTagsMapFromAnalysis(analysis, selectionFilter);
// Map each schema basename → tag subfolder ('shared' if used by 0 or >1 tags)
const schemaTagMap =
Object.keys(precomputedSchemaTagMap).length > 0
? precomputedSchemaTagMap
: buildSchemaTagMap(schemas, tagsMap);
// 1. Generate Models, Entities and Mappers from Schemas
Object.keys(schemas).forEach((schemaName) => {
const baseName = schemaName.replace(/Dto$/, '');
const tagFilename = schemaTagMap[baseName] || 'shared';
const schemaObj = schemas[schemaName] as OpenApiSchema;
const rawProperties = schemaObj.properties || {};
const requiredProps: string[] = schemaObj.required || [];
const varsMap = Object.keys(rawProperties).map((k) => {
let tsType = mapSwaggerTypeToTs(rawProperties[k].type);
const isInlineObject = rawProperties[k].type === 'object' && !rawProperties[k].$ref;
if (rawProperties[k].$ref) {
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
}
return {
name: safePropertyName(k),
originalName: k,
dataType: tsType,
description: rawProperties[k].description || '',
required: requiredProps.includes(k),
hasMockValue: !isInlineObject
};
});
// Collect imports for types referenced via $ref in properties
const referencedTypes = new Set<string>();
Object.values(rawProperties).forEach((prop) => {
if (prop.$ref) {
referencedTypes.add(prop.$ref.split('/').pop() || '');
} else if (prop.type === 'array' && prop.items?.$ref) {
referencedTypes.add(prop.items.$ref.split('/').pop() || '');
}
});
const modelImports = [...referencedTypes].filter(Boolean).map((name) => ({
classname: name,
classFilename: toCamelCase(name),
tagFilename: schemaTagMap[name] || 'shared'
}));
const modelViewData = {
tagFilename,
models: [
{
model: {
classname: baseName,
classFilename: toCamelCase(baseName),
classVarName: toCamelCase(baseName),
description: schemaObj.description || '',
imports: modelImports,
vars: varsMap
}
}
],
allModels: [{ model: { vars: varsMap } }]
};
const mapperViewData = {
...modelViewData,
apiInfo: {
apis: [
{
operations: {
classname: baseName,
classFilename: toCamelCase(baseName),
classVarName: toCamelCase(baseName),
tagFilename
}
}
]
}
};
// Model (Entities)
const modelTemplatePath = path.join(templatesDir, 'model-entity.mustache');
if (fs.existsSync(modelTemplatePath)) {
const template = fs.readFileSync(modelTemplatePath, 'utf8');
const output = mustache.render(template, modelViewData);
const destPath = path.join(
outputDir,
'entities/models',
tagFilename,
`${toCamelCase(baseName)}.model.ts`
);
fs.ensureDirSync(path.dirname(destPath));
fs.writeFileSync(destPath, output);
generatedCount.models++;
logDetail('generate', `model-entity → ${path.relative(process.cwd(), destPath)}`);
}
// Mapper
const mapperTemplatePath = path.join(templatesDir, 'mapper.mustache');
if (fs.existsSync(mapperTemplatePath)) {
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
const output = mustache.render(template, mapperViewData);
const destPath = path.join(
outputDir,
'data/mappers',
tagFilename,
`${toCamelCase(baseName)}.mapper.ts`
);
fs.ensureDirSync(path.dirname(destPath));
fs.writeFileSync(destPath, output);
generatedCount.mappers++;
}
// DTO mock — values resolved from raw schema (example, format, type)
const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
name: safePropertyName(k),
mockValue: resolveMockValue(k, rawProperties[k], 'dto', schemaName)
}));
const dtoMockImports = [...referencedTypes].filter(Boolean).map((name) => {
const targetTag = schemaTagMap[name] || 'shared';
const targetFile = `${toCamelCase(name)}.dto.mock`;
const importPath =
targetTag === tagFilename ? `./${targetFile}` : `../${targetTag}/${targetFile}`;
return {
classname: name,
classFilename: toCamelCase(name),
tagFilename: targetTag,
importPath
};
});
const dtoMockViewData = {
tagFilename,
models: [
{
model: {
classname: baseName,
classFilename: toCamelCase(baseName),
classVarName: toCamelCase(baseName),
mockImports: dtoMockImports,
vars: dtoMockVarsMap
}
}
]
};
renderTemplate(
templatesDir,
'dto.mock.mustache',
dtoMockViewData,
path.join(outputDir, 'data/dtos', tagFilename, `${toCamelCase(baseName)}.dto.mock.ts`),
generatedCount,
'mocks'
);
// Model mock — delegates to mapper + DTO mock (no property values needed)
renderTemplate(
templatesDir,
'model.mock.mustache',
modelViewData,
path.join(
outputDir,
'entities/models',
tagFilename,
`${toCamelCase(baseName)}.model.mock.ts`
),
generatedCount,
'mocks'
);
// Model spec
renderTemplate(
templatesDir,
'model-entity.spec.mustache',
modelViewData,
path.join(
outputDir,
'entities/models',
tagFilename,
`${toCamelCase(baseName)}.model.spec.ts`
),
generatedCount,
'specs'
);
// Mapper spec
renderTemplate(
templatesDir,
'mapper.spec.mustache',
mapperViewData,
path.join(outputDir, 'data/mappers', tagFilename, `${toCamelCase(baseName)}.mapper.spec.ts`),
generatedCount,
'specs'
);
});
// 2. Generate Use Cases and Repositories from Paths/Tags
// Generate per tag
Object.keys(tagsMap).forEach((tag) => {
const returnImports: { classname: string; classFilename: string; classVarName: string }[] = [];
const paramImports: { classname: string; classFilename: string; classVarName: string }[] = [];
const tagFilename = toCamelCase(tag);
const returnImports: {
classname: string;
classFilename: string;
classVarName: string;
tagFilename: string;
}[] = [];
const paramImports: {
classname: string;
classFilename: string;
classVarName: string;
tagFilename: string;
}[] = [];
Object.keys(schemas).forEach((s) => {
const usedAsReturn = tagsMap[tag].some(
@@ -347,7 +452,12 @@ export function generateCleanArchitecture(
op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`)
);
const entry = { classname: s, classFilename: toCamelCase(s), classVarName: toCamelCase(s) };
const entry = {
classname: s,
classFilename: toCamelCase(s),
classVarName: toCamelCase(s),
tagFilename: schemaTagMap[s] || 'shared'
};
if (usedAsReturn) {
returnImports.push(entry);
@@ -362,18 +472,14 @@ export function generateCleanArchitecture(
apis: [
{
operations: {
classname: tag,
classFilename: toCamelCase(tag),
classVarName: toCamelCase(tag),
classname: toPascalCase(tag),
classFilename: tagFilename,
classVarName: tagFilename,
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
operation: tagsMap[tag],
// All entity imports (return + param) — for contracts and use-cases
imports: [...returnImports, ...paramImports],
// Return-type-only imports — for repo impl (Dto + Entity + Mapper)
returnImports,
// Param-only imports — for repo impl (Entity only, no Dto/Mapper)
paramImports,
// Environment API key for the repository base URL (e.g. "aprovalmApi")
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
}
}
@@ -385,7 +491,7 @@ export function generateCleanArchitecture(
templatesDir,
'api.use-cases.contract.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.contract.ts`),
path.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.contract.ts`),
generatedCount,
'useCases'
);
@@ -394,7 +500,7 @@ export function generateCleanArchitecture(
templatesDir,
'api.use-cases.impl.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.ts`),
path.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.impl.ts`),
generatedCount,
'useCases'
);
@@ -403,7 +509,12 @@ export function generateCleanArchitecture(
templatesDir,
'api.repository.contract.mustache',
apiViewData,
path.join(outputDir, 'domain/repositories', `${toCamelCase(tag)}.repository.contract.ts`),
path.join(
outputDir,
'domain/repositories',
tagFilename,
`${tagFilename}.repository.contract.ts`
),
generatedCount,
'repositories'
);
@@ -412,7 +523,7 @@ export function generateCleanArchitecture(
templatesDir,
'api.repository.impl.mustache',
apiViewData,
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.ts`),
path.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.ts`),
generatedCount,
'repositories'
);
@@ -421,7 +532,7 @@ export function generateCleanArchitecture(
templatesDir,
'use-cases.provider.mustache',
apiViewData,
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.ts`),
path.join(outputDir, 'di/use-cases', tagFilename, `${tagFilename}.use-cases.provider.ts`),
generatedCount,
'providers'
);
@@ -430,17 +541,22 @@ export function generateCleanArchitecture(
templatesDir,
'repository.provider.mustache',
apiViewData,
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.ts`),
path.join(outputDir, 'di/repositories', tagFilename, `${tagFilename}.repository.provider.ts`),
generatedCount,
'providers'
);
// Mocks — repository impl, use-cases impl, repository provider, use-cases provider
// Mocks
renderTemplate(
templatesDir,
'api.repository.impl.mock.mustache',
apiViewData,
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.mock.ts`),
path.join(
outputDir,
'data/repositories',
tagFilename,
`${tagFilename}.repository.impl.mock.ts`
),
generatedCount,
'mocks'
);
@@ -449,7 +565,7 @@ export function generateCleanArchitecture(
templatesDir,
'api.use-cases.mock.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.mock.ts`),
path.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.mock.ts`),
generatedCount,
'mocks'
);
@@ -458,7 +574,12 @@ export function generateCleanArchitecture(
templatesDir,
'repository.provider.mock.mustache',
apiViewData,
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.mock.ts`),
path.join(
outputDir,
'di/repositories',
tagFilename,
`${tagFilename}.repository.provider.mock.ts`
),
generatedCount,
'mocks'
);
@@ -467,7 +588,12 @@ export function generateCleanArchitecture(
templatesDir,
'use-cases.provider.mock.mustache',
apiViewData,
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.mock.ts`),
path.join(
outputDir,
'di/use-cases',
tagFilename,
`${tagFilename}.use-cases.provider.mock.ts`
),
generatedCount,
'mocks'
);
@@ -477,7 +603,12 @@ export function generateCleanArchitecture(
templatesDir,
'api.repository.impl.spec.mustache',
apiViewData,
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.spec.ts`),
path.join(
outputDir,
'data/repositories',
tagFilename,
`${tagFilename}.repository.impl.spec.ts`
),
generatedCount,
'specs'
);
@@ -487,7 +618,12 @@ export function generateCleanArchitecture(
templatesDir,
'api.use-cases.impl.spec.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.spec.ts`),
path.join(
outputDir,
'domain/use-cases',
tagFilename,
`${tagFilename}.use-cases.impl.spec.ts`
),
generatedCount,
'specs'
);
@@ -512,6 +648,7 @@ function renderTemplate(
if (fs.existsSync(templatePath)) {
const template = fs.readFileSync(templatePath, 'utf8');
const output = mustache.render(template, viewData);
fs.ensureDirSync(path.dirname(destPath));
fs.writeFileSync(destPath, output);
counter[key]++;
logDetail(

View File

@@ -2,6 +2,7 @@ import { execSync } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
import { logStep, logSuccess, logError, logDetail } from '../utils/logger';
import { toPascalCase } from '../utils/name-formatter';
/** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
export function generateCode(swaggerFile: string, templatesDir: string): string {
@@ -35,8 +36,12 @@ export function generateCode(swaggerFile: string, templatesDir: string): string
}
}
/** Copies the generated DTOs from the temporary directory to the output directory. */
export function organizeFiles(tempDir: string, outputDir: string): void {
/** Copies the generated DTOs from the temporary directory to the output directory, organised by tag subfolder. */
export function organizeFiles(
tempDir: string,
outputDir: string,
schemaTagMap: Record<string, string> = {}
): void {
logStep('Organising generated DTO files...');
const sourceDir = path.join(tempDir, 'model');
@@ -49,8 +54,14 @@ export function organizeFiles(tempDir: string, outputDir: string): void {
const files = fs.readdirSync(sourceDir).filter((file) => file.endsWith('.dto.ts'));
files.forEach((file) => {
// file is like "userResponse.dto.ts" → derive PascalCase schema name to look up tag
const camelName = file.replace('.dto.ts', '');
const pascalName = toPascalCase(camelName);
const tagFolder = schemaTagMap[pascalName] || 'shared';
const sourcePath = path.join(sourceDir, file);
const destPath = path.join(destDir, file);
const destPath = path.join(destDir, tagFolder, file);
fs.ensureDirSync(path.dirname(destPath));
fs.copySync(sourcePath, destPath);
filesMoved++;
logDetail('dto', `${file}${path.relative(process.cwd(), destPath)}`);
@@ -65,58 +76,69 @@ export function addDtoImports(outputDir: string): void {
logStep('Post-processing generated DTOs...');
const dtosDir = path.join(outputDir, 'data/dtos');
if (!fs.existsSync(dtosDir)) return;
const files = fs.readdirSync(dtosDir).filter((f) => f.endsWith('.dto.ts'));
// Collect all .dto.ts files from all subfolders (1 level deep)
const allFiles: { subfolder: string; file: string; fullPath: string }[] = [];
// Build a map of DTO classname → file base name (without .ts)
const dtoMap: Record<string, string> = {};
files.forEach((file) => {
const content = fs.readFileSync(path.join(dtosDir, file), 'utf8');
const match = content.match(/export interface (\w+)/);
if (match) {
dtoMap[match[1]] = file.replace('.ts', '');
const entries = fs.readdirSync(dtosDir);
entries.forEach((entry) => {
const entryPath = path.join(dtosDir, entry);
if (fs.statSync(entryPath).isDirectory()) {
fs.readdirSync(entryPath)
.filter((f) => f.endsWith('.dto.ts'))
.forEach((file) =>
allFiles.push({ subfolder: entry, file, fullPath: path.join(entryPath, file) })
);
} else if (entry.endsWith('.dto.ts')) {
allFiles.push({ subfolder: '', file: entry, fullPath: entryPath });
}
});
// Build map: ClassName → { subfolder, fileBase }
const dtoMap: Record<string, { subfolder: string; fileBase: string }> = {};
allFiles.forEach(({ subfolder, file, fullPath }) => {
const content = fs.readFileSync(fullPath, 'utf8');
const match = content.match(/export interface (\w+)/);
if (match) dtoMap[match[1]] = { subfolder, fileBase: file.replace('.ts', '') };
});
let filesProcessed = 0;
files.forEach((file) => {
const filePath = path.join(dtosDir, file);
const originalContent = fs.readFileSync(filePath, 'utf8');
allFiles.forEach(({ subfolder, file, fullPath }) => {
const originalContent = fs.readFileSync(fullPath, 'utf8');
let content = originalContent;
const selfMatch = content.match(/export interface (\w+)/);
const selfName = selfMatch ? selfMatch[1] : '';
// Normalize Array<T> → T[] (openapi-generator-cli always outputs Array<T>)
content = content.replace(/Array<(\w+)>/g, '$1[]');
// Find all Dto type references in the file body (excluding the interface name itself)
const references = new Set<string>();
const typeRegex = /\b(\w+Dto)\b/g;
let match;
while ((match = typeRegex.exec(content)) !== null) {
if (match[1] !== selfName) {
references.add(match[1]);
}
if (match[1] !== selfName) references.add(match[1]);
}
// Build import lines for each referenced type that exists in the dtoMap
const imports: string[] = [];
references.forEach((ref) => {
if (dtoMap[ref]) {
imports.push(`import { ${ref} } from './${dtoMap[ref]}';`);
const { subfolder: refSubfolder, fileBase: refFileBase } = dtoMap[ref];
const fromDir = subfolder ? path.join(dtosDir, subfolder) : dtosDir;
const toFile = refSubfolder
? path.join(dtosDir, refSubfolder, refFileBase)
: path.join(dtosDir, refFileBase);
let relPath = path.relative(fromDir, toFile).replace(/\\/g, '/');
if (!relPath.startsWith('.')) relPath = './' + relPath;
imports.push(`import { ${ref} } from '${relPath}';`);
}
});
if (imports.length > 0) {
content = imports.join('\n') + '\n' + content;
}
if (imports.length > 0) content = imports.join('\n') + '\n' + content;
if (content !== originalContent) {
fs.writeFileSync(filePath, content);
fs.writeFileSync(fullPath, content);
filesProcessed++;
logDetail('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
}

View File

@@ -1,6 +1,7 @@
import fs from 'fs-extra';
import path from 'path';
import { logStep, logSuccess } from '../utils/logger';
import { getExampleMismatches } from '../utils/example-validator';
import type { SwaggerAnalysis, GenerationReport, LintResult } from '../types';
/** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
@@ -41,6 +42,8 @@ export function generateReport(
return { name: t.name, description: t.description || '', endpoints: endpointCount };
});
const exampleMismatches = getExampleMismatches();
const report: GenerationReport = {
timestamp: new Date().toISOString(),
tags: analysis.tags.length,
@@ -48,6 +51,10 @@ export function generateReport(
tagDetails,
outputDirectory: outputDir,
linting: lintResult,
warnings: {
exampleMismatches: exampleMismatches.map((m) => ({ ...m })),
total: exampleMismatches.length
},
structure: {
dtos: fs.readdirSync(path.join(outputDir, 'data/dtos')).length,
repositories: fs.readdirSync(path.join(outputDir, 'data/repositories')).length,

View File

@@ -28,6 +28,19 @@ export interface TagDetail {
endpoints: number;
}
/**
* A single example/type mismatch detected during mock generation.
*/
export interface ExampleMismatchEntry {
schemaName: string;
propertyName: string;
declaredType: string;
exampleValue: unknown;
exampleJsType: string;
action: 'coerced' | 'ignored';
coercedValue?: unknown;
}
/**
* Final generation report persisted as `generation-report.json`.
*/
@@ -38,6 +51,10 @@ export interface GenerationReport {
tagDetails: TagDetail[];
outputDirectory: string;
linting: LintResult;
warnings: {
exampleMismatches: ExampleMismatchEntry[];
total: number;
};
structure: {
dtos: number;
repositories: number;

View File

@@ -0,0 +1,103 @@
/**
* Validates that OpenAPI `example` values match their declared `type`.
*
* YAML parses unquoted values by native type (e.g. `example: 68131` becomes a JS number
* even when the schema declares `type: string`). This module detects such mismatches,
* coerces them when possible, and accumulates warnings for the generation report.
*/
export interface ExampleMismatch {
schemaName: string;
propertyName: string;
declaredType: string;
exampleValue: unknown;
exampleJsType: string;
action: 'coerced' | 'ignored';
coercedValue?: unknown;
}
// Module-level accumulator — reset between runs via `clearExampleMismatches()`.
let mismatches: ExampleMismatch[] = [];
/**
* Validates an `example` value against a declared OpenAPI `type`.
*
* @returns `{ valid: true }` when types already match, or
* `{ valid: false, coerced: <value> }` when the value was coerced, or
* `{ valid: false }` when coercion is not possible (caller should ignore the example).
*/
export function validateExample(
declaredType: string | undefined,
example: unknown
): { valid: boolean; coerced?: unknown } {
if (declaredType === undefined) return { valid: true };
const jsType = typeof example;
// ── string declared ──────────────────────────────────────────────────────
if (declaredType === 'string') {
if (jsType === 'string') return { valid: true };
// number or boolean → coerce to string
if (jsType === 'number' || jsType === 'boolean') {
return { valid: false, coerced: String(example) };
}
return { valid: false };
}
// ── integer / number declared ────────────────────────────────────────────
if (declaredType === 'integer' || declaredType === 'number') {
if (jsType === 'number') return { valid: true };
if (jsType === 'string') {
const parsed = Number(example);
if (!Number.isNaN(parsed)) return { valid: false, coerced: parsed };
return { valid: false }; // unparseable → ignore
}
return { valid: false };
}
// ── boolean declared ─────────────────────────────────────────────────────
if (declaredType === 'boolean') {
if (jsType === 'boolean') return { valid: true };
if (jsType === 'string') {
const lower = (example as string).toLowerCase();
if (lower === 'true') return { valid: false, coerced: true };
if (lower === 'false') return { valid: false, coerced: false };
}
return { valid: false }; // cannot coerce
}
// Other types (object, array, etc.) — no validation
return { valid: true };
}
/**
* Records a mismatch so it can be retrieved later for console warnings and the report.
*/
export function registerMismatch(
schemaName: string,
propertyName: string,
declaredType: string,
exampleValue: unknown,
action: 'coerced' | 'ignored',
coercedValue?: unknown
): void {
mismatches.push({
schemaName,
propertyName,
declaredType,
exampleValue,
exampleJsType: typeof exampleValue,
action,
coercedValue
});
}
/** Returns all recorded mismatches. */
export function getExampleMismatches(): ReadonlyArray<ExampleMismatch> {
return mismatches;
}
/** Clears all recorded mismatches (call before each generation run). */
export function clearExampleMismatches(): void {
mismatches = [];
}

View File

@@ -1,3 +1,5 @@
import { validateExample, registerMismatch } from './example-validator';
/**
* Resolves a TypeScript literal string to use as a mock value for a single schema property.
*
@@ -7,6 +9,7 @@
* @param propName Property name (used for format heuristics such as "email").
* @param prop Raw OpenAPI property definition.
* @param context 'dto' generates `mockFooDto()`, 'model' generates `mockFooModel()`.
* @param schemaName Parent schema name (used for mismatch reporting).
*/
export function resolveMockValue(
propName: string,
@@ -18,7 +21,8 @@ export function resolveMockValue(
$ref?: string;
items?: { $ref?: string; type?: string };
},
context: 'dto' | 'model' = 'dto'
context: 'dto' | 'model' = 'dto',
schemaName = 'unknown'
): string {
const suffix = context === 'dto' ? 'Dto' : 'Model';
@@ -43,8 +47,22 @@ export function resolveMockValue(
return typeof first === 'string' ? `'${first}'` : String(first);
}
// 5. Example value from the swagger spec (highest fidelity)
if (prop.example !== undefined) return formatLiteral(prop.example);
// 5. Example value — validated and coerced if needed
if (prop.example !== undefined) {
const result = validateExample(prop.type, prop.example);
if (result.valid) {
return formatLiteral(prop.example);
}
if (result.coerced !== undefined) {
registerMismatch(schemaName, propName, prop.type!, prop.example, 'coerced', result.coerced);
return formatLiteral(result.coerced);
}
// Cannot coerce — register and fall through to defaults
registerMismatch(schemaName, propName, prop.type!, prop.example, 'ignored');
}
// 6. Format-aware fallbacks (when no example is provided)
if (prop.format === 'date-time') return `'2024-01-01T00:00:00.000Z'`;

View File

@@ -1,12 +1,109 @@
/**
* Converts a PascalCase name to camelCase by lowercasing the first character.
* Converts a string to PascalCase, handling spaces, hyphens and underscores.
* Used to derive class names from schema/tag names.
*
* @example
* toPascalCase('Product Format') // 'ProductFormat'
* toPascalCase('user-response') // 'UserResponse'
* toPascalCase('UserSchema') // 'UserSchema'
*/
export function toPascalCase(name: string): string {
if (!name) return name;
return name
.split(/[\s\-_]+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
/**
* Converts a string to camelCase, handling spaces, hyphens and underscores.
* Used to derive class filenames and variable names from schema/tag names.
*
* @example
* toCamelCase('Product Format') // 'productFormat'
* toCamelCase('ProductResponse') // 'productResponse'
* toCamelCase('UserSchema') // 'userSchema'
*/
export function toCamelCase(name: string): string {
if (!name) return name;
return name.charAt(0).toLowerCase() + name.slice(1);
const pascal = toPascalCase(name);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
const JS_RESERVED_WORDS = new Set([
'abstract',
'arguments',
'await',
'boolean',
'break',
'byte',
'case',
'catch',
'char',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'double',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'final',
'finally',
'float',
'for',
'function',
'goto',
'if',
'implements',
'import',
'in',
'instanceof',
'int',
'interface',
'let',
'long',
'native',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'short',
'static',
'super',
'switch',
'synchronized',
'this',
'throw',
'throws',
'transient',
'true',
'try',
'typeof',
'undefined',
'var',
'void',
'volatile',
'while',
'with',
'yield'
]);
/** Returns true if the given name is a JS/TS reserved word. */
export function isReservedWord(name: string): boolean {
return JS_RESERVED_WORDS.has(name);
}
/** Prefixes reserved words with `_` to produce a safe identifier. */
export function safePropertyName(name: string): string {
return isReservedWord(name) ? `_${name}` : name;
}

View File

@@ -4,7 +4,7 @@
import { InjectionToken } from '@angular/core';
import { Observable } from 'rxjs';
{{#imports}}
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
{{/imports}}
/**

View File

@@ -4,9 +4,9 @@
import { MockService } from 'ng-mocks';
import { of } from 'rxjs';
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
{{/returnImports}}
export const mock{{classname}}RepositoryImpl = () =>

View File

@@ -9,14 +9,14 @@ import { environment } from '@environment';
import { MRepository } from '@mercadona/core/utils/repository';
import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
{{#returnImports}}
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
import { {{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
import { {{classVarName}}Mapper } from '@/mappers/{{tagFilename}}/{{classFilename}}.mapper';
{{/returnImports}}
{{#paramImports}}
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
{{/paramImports}}
/**

View File

@@ -6,8 +6,8 @@ import { TestBed } from '@angular/core/testing';
import { {{classname}}RepositoryImpl } from './{{classFilename}}.repository.impl';
{{#returnImports}}
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
{{/returnImports}}
describe('{{classname}}RepositoryImpl', () => {

View File

@@ -4,7 +4,7 @@
import { InjectionToken } from '@angular/core';
import { Observable } from 'rxjs';
{{#imports}}
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
{{/imports}}
/**

View File

@@ -6,9 +6,9 @@ import { Observable } from 'rxjs';
import { {{classname}}UseCases } from './{{classFilename}}.use-cases.contract';
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
{{#imports}}
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
{{/imports}}
/**

View File

@@ -6,9 +6,9 @@ import { of } from 'rxjs';
import { {{classname}}UseCasesImpl } from './{{classFilename}}.use-cases.impl';
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
{{/returnImports}}
describe('{{classname}}UseCasesImpl', () => {

View File

@@ -4,9 +4,9 @@
import { MockService } from 'ng-mocks';
import { of } from 'rxjs';
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
{{/returnImports}}
export const mock{{classname}}UseCasesImpl = () =>

View File

@@ -1,7 +1,7 @@
{{#models}}
{{#model}}
{{#mockImports}}
import { mock{{classname}}Dto } from './{{classFilename}}.dto.mock';
import { mock{{classname}}Dto } from '{{{importPath}}}';
{{/mockImports}}
import { {{classname}}Dto } from './{{classFilename}}.dto';

View File

@@ -4,8 +4,8 @@
import { MapFromFn } from '@mercadona/common/public';
import { Builder } from '@mercadona/common/utils';
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
import { {{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
/**
* {{classname}} Mapper

View File

@@ -2,8 +2,8 @@
{{#model}}
import { {{classVarName}}Mapper } from './{{classFilename}}.mapper';
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
describe('{{classVarName}}Mapper', () => {
{{#vars}}

View File

@@ -1,7 +1,7 @@
{{#models}}
{{#model}}
{{#imports}}
import { {{classname}} } from './{{classFilename}}.model';
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
{{/imports}}
/**

View File

@@ -11,6 +11,7 @@ describe('{{classname}}', () => {
});
{{#vars}}
{{#hasMockValue}}
it('should allow setting {{name}}', () => {
const model = new {{classname}}();
const expected = mock{{classname}}Model();
@@ -19,13 +20,16 @@ describe('{{classname}}', () => {
expect(model.{{name}}).toBe(expected.{{name}});
});
{{/hasMockValue}}
{{/vars}}
it('should build a valid model from mock', () => {
const model = mock{{classname}}Model();
expect(model).toBeInstanceOf({{classname}});
{{#vars}}
{{#hasMockValue}}
expect(model.{{name}}).toBeDefined();
{{/hasMockValue}}
{{/vars}}
});
});

View File

@@ -1,8 +1,8 @@
{{#models}}
{{#model}}
import { {{classname}} } from './{{classFilename}}.model';
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
import { {{classVarName}}Mapper } from '@/mappers/{{tagFilename}}/{{classFilename}}.mapper';
import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
export const mock{{classname}}Model = (overrides: Partial<{{classname}}> = {}): {{classname}} =>
Object.assign(new {{classname}}(), {

View File

@@ -3,8 +3,8 @@
{{#operations}}
import { Provider } from '@angular/core';
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract';
import { mock{{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl.mock';
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
import { mock{{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl.mock';
export function mock{{classname}}Repository(): Provider[] {
return [

View File

@@ -3,8 +3,8 @@
{{#operations}}
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract';
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl';
/**
* {{classname}} Repository Provider

View File

@@ -3,8 +3,8 @@
{{#operations}}
import { Provider } from '@angular/core';
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
import { mock{{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.mock';
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
import { mock{{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.mock';
export function mock{{classname}}UseCases(): Provider[] {
return [

View File

@@ -3,8 +3,8 @@
{{#operations}}
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
/**
* {{classname}} Use Cases Provider