feat: add mock generation for DTOs, models, and repositories with corresponding templates
This commit is contained in:
1
main.ts
1
main.ts
@@ -143,6 +143,7 @@ async function main(): Promise<void> {
|
||||
console.log(` - Mappers: ${report.structure.mappers}`);
|
||||
console.log(` - Use Cases: ${report.structure.useCases}`);
|
||||
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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import mustache from 'mustache';
|
||||
import { logStep, logSuccess, logInfo } from '../utils/logger';
|
||||
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
|
||||
import { toCamelCase } from '../utils/name-formatter';
|
||||
import { resolveMockValue } from '../utils/mock-value-resolver';
|
||||
import type {
|
||||
SwaggerAnalysis,
|
||||
OpenApiSchema,
|
||||
@@ -72,7 +73,8 @@ export function generateCleanArchitecture(
|
||||
repositories: 0,
|
||||
mappers: 0,
|
||||
useCases: 0,
|
||||
providers: 0
|
||||
providers: 0,
|
||||
mocks: 0
|
||||
};
|
||||
|
||||
const schemas =
|
||||
@@ -166,6 +168,48 @@ export function generateCleanArchitecture(
|
||||
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'
|
||||
);
|
||||
});
|
||||
|
||||
// 2. Generate Use Cases and Repositories from Paths/Tags
|
||||
@@ -357,10 +401,47 @@ export function generateCleanArchitecture(
|
||||
generatedCount,
|
||||
'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(
|
||||
`${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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,15 @@ import path from 'path';
|
||||
import { logStep, logSuccess } from '../utils/logger';
|
||||
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. */
|
||||
export function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport {
|
||||
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,
|
||||
providers:
|
||||
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'))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface GeneratedCount {
|
||||
mappers: number;
|
||||
useCases: number;
|
||||
providers: number;
|
||||
mocks: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,5 +24,6 @@ export interface GenerationReport {
|
||||
mappers: number;
|
||||
useCases: number;
|
||||
providers: number;
|
||||
mocks: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,9 +30,12 @@ export interface OpenApiSchema {
|
||||
string,
|
||||
{
|
||||
type?: string;
|
||||
format?: string;
|
||||
description?: string;
|
||||
example?: unknown;
|
||||
enum?: unknown[];
|
||||
$ref?: string;
|
||||
items?: { $ref?: string };
|
||||
items?: { $ref?: string; type?: string };
|
||||
}
|
||||
>;
|
||||
required?: string[];
|
||||
|
||||
70
src/utils/mock-value-resolver.ts
Normal file
70
src/utils/mock-value-resolver.ts
Normal 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)}'`;
|
||||
}
|
||||
21
templates/api.repository.impl.mock.mustache
Normal file
21
templates/api.repository.impl.mock.mustache
Normal 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}}
|
||||
21
templates/api.use-cases.mock.mustache
Normal file
21
templates/api.use-cases.mock.mustache
Normal 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}}
|
||||
16
templates/dto.mock.mustache
Normal file
16
templates/dto.mock.mustache
Normal 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}}
|
||||
14
templates/model.mock.mustache
Normal file
14
templates/model.mock.mustache
Normal 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}}
|
||||
20
templates/repository.provider.mock.mustache
Normal file
20
templates/repository.provider.mock.mustache
Normal 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}}
|
||||
20
templates/use-cases.provider.mock.mustache
Normal file
20
templates/use-cases.provider.mock.mustache
Normal 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}}
|
||||
Reference in New Issue
Block a user