diff --git a/main.ts b/main.ts index 92861c7..10b09e4 100755 --- a/main.ts +++ b/main.ts @@ -143,6 +143,7 @@ async function main(): Promise { 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`); } diff --git a/src/generators/clean-arch.generator.ts b/src/generators/clean-arch.generator.ts index a9e10c8..c6d1d17 100644 --- a/src/generators/clean-arch.generator.ts +++ b/src/generators/clean-arch.generator.ts @@ -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; } diff --git a/src/generators/report.generator.ts b/src/generators/report.generator.ts index 166bf0f..f8386f7 100644 --- a/src/generators/report.generator.ts +++ b/src/generators/report.generator.ts @@ -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')) } }; diff --git a/src/types/generation.types.ts b/src/types/generation.types.ts index 56f8d9d..55f5368 100644 --- a/src/types/generation.types.ts +++ b/src/types/generation.types.ts @@ -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; }; } diff --git a/src/types/openapi.types.ts b/src/types/openapi.types.ts index a9a1bb3..02498f2 100644 --- a/src/types/openapi.types.ts +++ b/src/types/openapi.types.ts @@ -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[]; diff --git a/src/utils/mock-value-resolver.ts b/src/utils/mock-value-resolver.ts new file mode 100644 index 0000000..0112c89 --- /dev/null +++ b/src/utils/mock-value-resolver.ts @@ -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)}'`; +} diff --git a/templates/api.repository.impl.mock.mustache b/templates/api.repository.impl.mock.mustache new file mode 100644 index 0000000..040c7e8 --- /dev/null +++ b/templates/api.repository.impl.mock.mustache @@ -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}} diff --git a/templates/api.use-cases.mock.mustache b/templates/api.use-cases.mock.mustache new file mode 100644 index 0000000..65e1b2d --- /dev/null +++ b/templates/api.use-cases.mock.mustache @@ -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}} diff --git a/templates/dto.mock.mustache b/templates/dto.mock.mustache new file mode 100644 index 0000000..2c19193 --- /dev/null +++ b/templates/dto.mock.mustache @@ -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}} diff --git a/templates/model.mock.mustache b/templates/model.mock.mustache new file mode 100644 index 0000000..b9646e7 --- /dev/null +++ b/templates/model.mock.mustache @@ -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}} diff --git a/templates/repository.provider.mock.mustache b/templates/repository.provider.mock.mustache new file mode 100644 index 0000000..f889d57 --- /dev/null +++ b/templates/repository.provider.mock.mustache @@ -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}} diff --git a/templates/use-cases.provider.mock.mustache b/templates/use-cases.provider.mock.mustache new file mode 100644 index 0000000..88abe90 --- /dev/null +++ b/templates/use-cases.provider.mock.mustache @@ -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}}