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(` - 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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
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