diff --git a/main.ts b/main.ts
index 92861c7..6f5331f 100755
--- a/main.ts
+++ b/main.ts
@@ -15,6 +15,7 @@ import {
extractTagsWithOperations
} 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 { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
import type { SelectionFilter } from './src/types';
@@ -37,6 +38,7 @@ program
.option('-t, --templates
', 'Custom templates directory', path.join(__dirname, 'templates'))
.option('--skip-install', 'Skip dependency installation')
.option('--dry-run', 'Simulate without generating files')
+ .option('--skip-lint', 'Skip post-generation linting and formatting')
.option('-s, --select-endpoints', 'Interactively select which tags and endpoints to generate')
.parse(process.argv);
@@ -132,6 +134,10 @@ async function main(): Promise {
);
cleanup(tempDir);
+ if (!options.skipLint) {
+ lintGeneratedFiles(options.output);
+ }
+
const report = generateReport(options.output, analysis);
console.log('\n' + '='.repeat(60));
@@ -143,6 +149,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/lint.generator.ts b/src/generators/lint.generator.ts
new file mode 100644
index 0000000..2ac81f1
--- /dev/null
+++ b/src/generators/lint.generator.ts
@@ -0,0 +1,106 @@
+import fs from 'fs-extra';
+import path from 'path';
+import { spawnSync } from 'child_process';
+import { logStep, logSuccess, logWarning, logInfo } from '../utils/logger';
+
+/**
+ * Walks up the directory tree from `startDir` to find the nearest
+ * directory containing a `package.json` (i.e. the project root).
+ * Returns `null` if none is found before reaching the filesystem root.
+ */
+function findProjectRoot(startDir: string): string | null {
+ let current = path.resolve(startDir);
+ while (true) {
+ if (fs.existsSync(path.join(current, 'package.json'))) return current;
+ const parent = path.dirname(current);
+ if (parent === current) return null;
+ current = parent;
+ }
+}
+
+/**
+ * Collects all `.ts` files recursively inside a directory.
+ */
+function collectTsFiles(dir: string): string[] {
+ if (!fs.existsSync(dir)) return [];
+ const results: string[] = [];
+ fs.readdirSync(dir).forEach((entry) => {
+ const full = path.join(dir, entry);
+ if (fs.statSync(full).isDirectory()) {
+ results.push(...collectTsFiles(full));
+ } else if (entry.endsWith('.ts')) {
+ results.push(full);
+ }
+ });
+ return results;
+}
+
+/**
+ * Runs a command synchronously and returns whether it succeeded.
+ * Prints stdout/stderr to the console only on failure.
+ */
+function run(cmd: string, args: string[], cwd: string): boolean {
+ const result = spawnSync(cmd, args, { cwd, encoding: 'utf8', shell: true });
+ if (result.status !== 0) {
+ if (result.stderr) process.stderr.write(result.stderr);
+ if (result.stdout) process.stdout.write(result.stdout);
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Runs Prettier and ESLint (--fix) on all generated `.ts` files inside `outputDir`.
+ * Both tools are looked up via `npx` in the nearest project root so that the
+ * target Angular project's own configuration and plugins are used.
+ *
+ * - Prettier: always attempted; logs a warning if not found.
+ * - ESLint: optional; silently skipped if no config is found in the project root.
+ */
+export function lintGeneratedFiles(outputDir: string): void {
+ logStep('Linting generated files...');
+
+ const projectRoot = findProjectRoot(outputDir);
+ if (!projectRoot) {
+ logWarning('Could not locate a project root (package.json). Skipping lint.');
+ return;
+ }
+ logInfo(` Project root: ${projectRoot}`);
+
+ const files = collectTsFiles(outputDir);
+ if (files.length === 0) {
+ logWarning('No TypeScript files found in output directory. Skipping lint.');
+ return;
+ }
+
+ const relativePaths = files.map((f) => path.relative(projectRoot, f));
+
+ // --- Prettier ---
+ const prettierOk = run('npx', ['prettier', '--write', ...relativePaths], projectRoot);
+ if (prettierOk) {
+ logSuccess(`Prettier formatted ${files.length} files`);
+ } else {
+ logWarning('Prettier not available or encountered errors. Skipping formatting.');
+ }
+
+ // --- ESLint (only if a config exists in the project root) ---
+ const hasEslintConfig =
+ fs.existsSync(path.join(projectRoot, 'eslint.config.js')) ||
+ fs.existsSync(path.join(projectRoot, 'eslint.config.mjs')) ||
+ fs.existsSync(path.join(projectRoot, '.eslintrc.js')) ||
+ fs.existsSync(path.join(projectRoot, '.eslintrc.json')) ||
+ fs.existsSync(path.join(projectRoot, '.eslintrc.yml')) ||
+ fs.existsSync(path.join(projectRoot, '.eslintrc.yaml'));
+
+ if (!hasEslintConfig) {
+ logWarning('No ESLint config found in project root. Skipping ESLint fix.');
+ return;
+ }
+
+ const eslintOk = run('npx', ['eslint', '--fix', ...relativePaths], projectRoot);
+ if (eslintOk) {
+ logSuccess(`ESLint fixed ${files.length} files`);
+ } else {
+ logWarning('ESLint reported errors that could not be auto-fixed. Review the output above.');
+ }
+}
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/cli.types.ts b/src/types/cli.types.ts
index bea1609..bc2605a 100644
--- a/src/types/cli.types.ts
+++ b/src/types/cli.types.ts
@@ -9,4 +9,5 @@ export interface CliOptions {
skipInstall?: boolean;
dryRun?: boolean;
selectEndpoints?: boolean;
+ skipLint?: boolean;
}
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-entity.mustache b/templates/model-entity.mustache
index 65fbc8f..34fd5f4 100644
--- a/templates/model-entity.mustache
+++ b/templates/model-entity.mustache
@@ -16,7 +16,7 @@ export class {{classname}} {
* {{description}}
*/
{{/description}}
- {{name}}{{^required}}?{{/required}}: {{{dataType}}};
+ {{name}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{{dataType}}};
{{/vars}}
}
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}}