feat: add mock generation for DTOs, models, and repositories with corresponding templates

This commit is contained in:
didavila
2026-03-25 11:01:21 +01:00
parent a0a8ba28f5
commit 917cc3b9cf
12 changed files with 289 additions and 4 deletions

View File

@@ -143,6 +143,7 @@ async function main(): Promise<void> {
console.log(` - Mappers: ${report.structure.mappers}`); console.log(` - Mappers: ${report.structure.mappers}`);
console.log(` - Use Cases: ${report.structure.useCases}`); console.log(` - Use Cases: ${report.structure.useCases}`);
console.log(` - Providers: ${report.structure.providers}`); console.log(` - Providers: ${report.structure.providers}`);
console.log(` - Mocks: ${report.structure.mocks}`);
console.log(`\n📁 Files generated in: ${colors.cyan}${options.output}${colors.reset}\n`); console.log(`\n📁 Files generated in: ${colors.cyan}${options.output}${colors.reset}\n`);
} }

View File

@@ -4,6 +4,7 @@ import mustache from 'mustache';
import { logStep, logSuccess, logInfo } from '../utils/logger'; import { logStep, logSuccess, logInfo } from '../utils/logger';
import { mapSwaggerTypeToTs } from '../utils/type-mapper'; import { mapSwaggerTypeToTs } from '../utils/type-mapper';
import { toCamelCase } from '../utils/name-formatter'; import { toCamelCase } from '../utils/name-formatter';
import { resolveMockValue } from '../utils/mock-value-resolver';
import type { import type {
SwaggerAnalysis, SwaggerAnalysis,
OpenApiSchema, OpenApiSchema,
@@ -72,7 +73,8 @@ export function generateCleanArchitecture(
repositories: 0, repositories: 0,
mappers: 0, mappers: 0,
useCases: 0, useCases: 0,
providers: 0 providers: 0,
mocks: 0
}; };
const schemas = const schemas =
@@ -166,6 +168,48 @@ export function generateCleanArchitecture(
fs.writeFileSync(destPath, output); fs.writeFileSync(destPath, output);
generatedCount.mappers++; 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'
);
}); });
// 2. Generate Use Cases and Repositories from Paths/Tags // 2. Generate Use Cases and Repositories from Paths/Tags
@@ -357,10 +401,47 @@ export function generateCleanArchitecture(
generatedCount, generatedCount,
'providers' 'providers'
); );
// Mocks — repository impl, use-cases impl, repository provider, use-cases provider
renderTemplate(
templatesDir,
'api.repository.impl.mock.mustache',
apiViewData,
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.mock.ts`),
generatedCount,
'mocks'
);
renderTemplate(
templatesDir,
'api.use-cases.mock.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.mock.ts`),
generatedCount,
'mocks'
);
renderTemplate(
templatesDir,
'repository.provider.mock.mustache',
apiViewData,
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.mock.ts`),
generatedCount,
'mocks'
);
renderTemplate(
templatesDir,
'use-cases.provider.mock.mustache',
apiViewData,
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.mock.ts`),
generatedCount,
'mocks'
);
}); });
logSuccess( logSuccess(
`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers generados con Mustache` `${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks generated`
); );
return generatedCount; return generatedCount;
} }

View File

@@ -3,6 +3,15 @@ import path from 'path';
import { logStep, logSuccess } from '../utils/logger'; import { logStep, logSuccess } from '../utils/logger';
import type { SwaggerAnalysis, GenerationReport } from '../types'; import type { SwaggerAnalysis, GenerationReport } from '../types';
/** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
function countMockFiles(dir: string): number {
try {
return fs.readdirSync(dir).filter((f) => f.endsWith('.mock.ts')).length;
} catch {
return 0;
}
}
/** Generates and persists the `generation-report.json` file with process statistics. */ /** Generates and persists the `generation-report.json` file with process statistics. */
export function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport { export function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport {
logStep('Generating report...'); logStep('Generating report...');
@@ -19,7 +28,14 @@ export function generateReport(outputDir: string, analysis: SwaggerAnalysis): Ge
useCases: fs.readdirSync(path.join(outputDir, 'domain/use-cases')).length, useCases: fs.readdirSync(path.join(outputDir, 'domain/use-cases')).length,
providers: providers:
fs.readdirSync(path.join(outputDir, 'di/repositories')).length + fs.readdirSync(path.join(outputDir, 'di/repositories')).length +
fs.readdirSync(path.join(outputDir, 'di/use-cases')).length fs.readdirSync(path.join(outputDir, 'di/use-cases')).length,
mocks:
countMockFiles(path.join(outputDir, 'data/dtos')) +
countMockFiles(path.join(outputDir, 'data/repositories')) +
countMockFiles(path.join(outputDir, 'di/repositories')) +
countMockFiles(path.join(outputDir, 'di/use-cases')) +
countMockFiles(path.join(outputDir, 'domain/use-cases')) +
countMockFiles(path.join(outputDir, 'entities/models'))
} }
}; };

View File

@@ -7,6 +7,7 @@ export interface GeneratedCount {
mappers: number; mappers: number;
useCases: number; useCases: number;
providers: number; providers: number;
mocks: number;
} }
/** /**
@@ -23,5 +24,6 @@ export interface GenerationReport {
mappers: number; mappers: number;
useCases: number; useCases: number;
providers: number; providers: number;
mocks: number;
}; };
} }

View File

@@ -30,9 +30,12 @@ export interface OpenApiSchema {
string, string,
{ {
type?: string; type?: string;
format?: string;
description?: string; description?: string;
example?: unknown;
enum?: unknown[];
$ref?: string; $ref?: string;
items?: { $ref?: string }; items?: { $ref?: string; type?: string };
} }
>; >;
required?: string[]; required?: string[];

View File

@@ -0,0 +1,70 @@
/**
* Resolves a TypeScript literal string to use as a mock value for a single schema property.
*
* Priority chain:
* $ref mock call → array $ref mock call → enum[0] → example → format fallback → type default
*
* @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()`.
*/
export function resolveMockValue(
propName: string,
prop: {
type?: string;
format?: string;
example?: unknown;
enum?: unknown[];
$ref?: string;
items?: { $ref?: string; type?: string };
},
context: 'dto' | 'model' = 'dto'
): string {
const suffix = context === 'dto' ? 'Dto' : 'Model';
// 1. Direct $ref → call the referenced mock factory
if (prop.$ref) {
const refName = prop.$ref.split('/').pop()!;
return `mock${refName}${suffix}()`;
}
// 2. Array of $ref → wrap referenced mock in an array
if (prop.type === 'array' && prop.items?.$ref) {
const refName = prop.items.$ref.split('/').pop()!;
return `[mock${refName}${suffix}()]`;
}
// 3. Array of primitives
if (prop.type === 'array') return '[]';
// 4. Enum → first declared value
if (prop.enum?.length) {
const first = prop.enum[0];
return typeof first === 'string' ? `'${first}'` : String(first);
}
// 5. Example value from the swagger spec (highest fidelity)
if (prop.example !== undefined) return formatLiteral(prop.example);
// 6. Format-aware fallbacks (when no example is provided)
if (prop.format === 'date-time') return `'2024-01-01T00:00:00.000Z'`;
if (prop.format === 'date') return `'2024-01-01'`;
if (prop.format === 'uuid') return `'00000000-0000-0000-0000-000000000000'`;
if (prop.format === 'uri') return `'https://example.com'`;
if (prop.format === 'email' || propName.toLowerCase().includes('email'))
return `'user@example.com'`;
// 7. Type defaults
if (prop.type === 'string') return `'value'`;
if (prop.type === 'integer' || prop.type === 'number') return `0`;
if (prop.type === 'boolean') return `false`;
return 'undefined';
}
function formatLiteral(value: unknown): string {
if (typeof value === 'string') return `'${value}'`;
if (typeof value === 'number') return `${value}`;
if (typeof value === 'boolean') return `${value}`;
return `'${String(value)}'`;
}

View File

@@ -0,0 +1,21 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { MockService } from 'ng-mocks';
import { of } from 'rxjs';
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
{{/returnImports}}
export const mock{{classname}}RepositoryImpl = () =>
MockService({{classname}}RepositoryImpl, {
{{#operation}}
{{nickname}}: () => of({{#isListContainer}}[mock{{returnBaseType}}Model()]{{/isListContainer}}{{^isListContainer}}{{#returnBaseType}}mock{{returnBaseType}}Model(){{/returnBaseType}}{{^returnBaseType}}undefined{{/returnBaseType}}{{/isListContainer}}),
{{/operation}}
});
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -0,0 +1,21 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
import { MockService } from 'ng-mocks';
import { of } from 'rxjs';
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
{{#returnImports}}
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
{{/returnImports}}
export const mock{{classname}}UseCasesImpl = () =>
MockService({{classname}}UseCasesImpl, {
{{#operation}}
{{nickname}}: () => of({{#isListContainer}}[mock{{returnBaseType}}Model()]{{/isListContainer}}{{^isListContainer}}{{#returnBaseType}}mock{{returnBaseType}}Model(){{/returnBaseType}}{{^returnBaseType}}undefined{{/returnBaseType}}{{/isListContainer}}),
{{/operation}}
});
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -0,0 +1,16 @@
{{#models}}
{{#model}}
{{#mockImports}}
import { mock{{classname}}Dto } from './{{classFilename}}.dto.mock';
{{/mockImports}}
import { {{classname}}Dto } from './{{classFilename}}.dto';
export const mock{{classname}}Dto = (overrides: Partial<{{classname}}Dto> = {}): {{classname}}Dto => ({
{{#vars}}
{{name}}: {{{mockValue}}},
{{/vars}}
...overrides
});
{{/model}}
{{/models}}

View File

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

View File

@@ -0,0 +1,20 @@
{{#apiInfo}}
{{#apis}}
{{#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';
export function mock{{classname}}Repository(): Provider[] {
return [
{
provide: {{constantName}}_REPOSITORY,
useFactory: () => mock{{classname}}RepositoryImpl()
}
];
}
{{/operations}}
{{/apis}}
{{/apiInfo}}

View File

@@ -0,0 +1,20 @@
{{#apiInfo}}
{{#apis}}
{{#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';
export function mock{{classname}}UseCases(): Provider[] {
return [
{
provide: {{constantName}}_USE_CASES,
useFactory: () => mock{{classname}}UseCasesImpl()
}
];
}
{{/operations}}
{{/apis}}
{{/apiInfo}}