commit 5ff88d8cf6ac2d6aa950e1e8ef4ee181eabb8218 Author: Blas Santome Ocampo Date: Mon Mar 23 09:35:15 2026 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d47a97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Dependencias +node_modules/ +package-lock.json + +# Archivos temporales de generación +.temp-generated/ +temp-generated/ + +# Reportes +generation-report.json + +# Logs +*.log +npm-debug.log* + +# Sistema operativo +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Output de prueba +test-output/ diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore new file mode 100644 index 0000000..e1f72fa --- /dev/null +++ b/.openapi-generator-ignore @@ -0,0 +1,33 @@ +# OpenAPI Generator Ignore File +# Archivos que no queremos generar + +# Documentación +README.md +.gitignore +git_push.sh + +# NPM +.npmignore +package.json +package-lock.json + +# TypeScript config +tsconfig.json +tsconfig.spec.json + +# Angular specific +angular.json +karma.conf.js + +# Tests que no usamos +*.spec.ts + +# Variables de entorno +variables.ts +configuration.ts + +# Encoder +encoder.ts + +# API base que usamos nuestra propia implementación +api.ts diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..c14326c --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,157 @@ +# 🚀 Guía de Inicio Rápido + +## Instalación en 3 pasos + +### 1. Instalar dependencias + +```bash +npm install +``` + +### 2. Instalar OpenAPI Generator CLI + +```bash +npm run setup +# O manualmente: +npm install -g @openapitools/openapi-generator-cli +``` + +### 3. Probar con el ejemplo incluido + +```bash +node generate.js -i example-swagger.yaml -o ./test-output --dry-run +``` + +## Uso con tu API + +### Paso 1: Copia tu archivo Swagger/OpenAPI + +```bash +cp /ruta/a/tu/api.yaml ./swagger.yaml +``` + +### Paso 2: Genera el código + +```bash +node generate.js -i swagger.yaml -o ./src/app +``` + +### Paso 3: Revisa los archivos generados + +```bash +ls -la ./src/app/data/ +ls -la ./src/app/domain/ +ls -la ./src/app/di/ +``` + +## Opciones Comunes + +### Generar en otra carpeta + +```bash +node generate.js -i swagger.yaml -o ./frontend/src/app +``` + +### Usar templates personalizados + +```bash +# Edita los archivos en ./templates/ +# Luego ejecuta: +node generate.js -i swagger.yaml -t ./templates +``` + +### Modo de prueba (sin generar archivos) + +```bash +node generate.js -i swagger.yaml --dry-run +``` + +## Integración con tu proyecto Angular + +### 1. Registra los providers + +En `app.config.ts` o `app.module.ts`: + +```typescript +import { UserRepositoryProvider } from '@/di/repositories/user.repository.provider'; +import { UserUseCasesProvider } from '@/di/use-cases/user.use-cases.provider'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... otros providers + UserRepositoryProvider, + UserUseCasesProvider + ] +}; +``` + +### 2. Configura los path aliases + +En `tsconfig.json`: + +```json +{ + "compilerOptions": { + "paths": { + "@/*": ["src/app/*"], + "@environment": ["src/environments/environment"] + } + } +} +``` + +### 3. Usa en tus componentes + +```typescript +import { Component, inject } from '@angular/core'; +import { USER_USE_CASES } from '@/domain/use-cases/user/user.use-cases.contract'; + +@Component({ + selector: 'app-users', + template: `...` +}) +export class UsersComponent { + #userUseCases = inject(USER_USE_CASES); + + ngOnInit() { + this.#userUseCases.getUsers().subscribe(users => { + console.log(users); + }); + } +} +``` + +## Troubleshooting + +### ❌ Error: openapi-generator-cli: command not found + +**Solución:** +```bash +npm install -g @openapitools/openapi-generator-cli +``` + +### ❌ Error: Cannot find module 'commander' + +**Solución:** +```bash +npm install +``` + +### ❌ Los archivos no se generan + +**Solución:** Verifica que el directorio de salida existe o usa `--dry-run` para ver qué pasaría: +```bash +node generate.js --dry-run +``` + +## Próximos pasos + +1. ✅ Genera el código desde tu Swagger +2. 📝 Ajusta los templates según tus necesidades +3. 🔧 Configura los path aliases en tu tsconfig.json +4. 📦 Registra los providers en tu módulo Angular +5. 🚀 ¡Usa el código generado en tus componentes! + +## ¿Necesitas ayuda? + +Consulta el README.md completo para documentación detallada. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d6b1b5 --- /dev/null +++ b/README.md @@ -0,0 +1,286 @@ +# OpenAPI Clean Architecture Generator + +Generador de código Angular con Clean Architecture desde archivos OpenAPI/Swagger. + +## 🚀 Instalación + +### Opción 1: Instalación Global + +```bash +npm install -g @openapitools/openapi-generator-cli +npm install +``` + +### Opción 2: Usar directamente + +```bash +npm install +npm run setup +``` + +## 📖 Uso + +### Comando básico + +```bash +node generate.js -i swagger.yaml +``` + +### Opciones disponibles + +```bash +node generate.js [opciones] + +Opciones: + -V, --version Mostrar versión + -i, --input Archivo OpenAPI/Swagger (yaml o json) [default: swagger.yaml] + -o, --output Directorio de salida [default: ./src/app] + -t, --templates Directorio de templates personalizados [default: ./templates] + --skip-install No instalar dependencias + --dry-run Simular sin generar archivos + -h, --help Mostrar ayuda +``` + +### Ejemplos + +```bash +# Generar desde swagger.yaml en src/app +node generate.js -i swagger.yaml -o ./src/app + +# Usar templates personalizados +node generate.js -i api.yaml -t ./mis-templates + +# Modo de prueba (no genera archivos) +node generate.js -i swagger.yaml --dry-run + +# Especificar todos los parámetros +node generate.js -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates +``` + +## 📁 Estructura Generada + +El generador crea la siguiente estructura siguiendo Clean Architecture: + +``` +src/app/ +├── data/ # Capa de datos +│ ├── dtos/ # Data Transfer Objects +│ │ ├── node/ +│ │ │ └── node.dto.ts +│ │ ├── order-type/ +│ │ │ └── order-type.dto.ts +│ │ └── supply-mode/ +│ │ └── supply-mode.dto.ts +│ ├── repositories/ # Implementaciones de repositorios +│ │ ├── node.repository.impl.ts +│ │ ├── order-type.repository.impl.ts +│ │ └── supply-mode.repository.impl.ts +│ └── mappers/ # Transformadores DTO → Entidad +│ ├── node.mapper.ts +│ ├── order-type.mapper.ts +│ └── supply-mode.mapper.ts +├── domain/ # Capa de dominio +│ ├── repositories/ # Contratos de repositorios +│ │ ├── node.repository.contract.ts +│ │ ├── order-type.repository.contract.ts +│ │ └── supply-mode.repository.contract.ts +│ └── use-cases/ # Casos de uso +│ ├── node/ +│ │ ├── node.use-cases.contract.ts +│ │ └── node.use-cases.impl.ts +│ ├── order-type/ +│ │ ├── order-type.use-cases.contract.ts +│ │ └── order-type.use-cases.impl.ts +│ └── supply-mode/ +│ ├── supply-mode.use-cases.contract.ts +│ └── supply-mode.use-cases.impl.ts +├── di/ # Inyección de dependencias +│ ├── repositories/ # Providers de repositorios +│ │ ├── node.repository.provider.ts +│ │ ├── order-type.repository.provider.ts +│ │ └── supply-mode.repository.provider.ts +│ └── use-cases/ # Providers de use cases +│ ├── node.use-cases.provider.ts +│ ├── order-type.use-cases.provider.ts +│ └── supply-mode.use-cases.provider.ts +└── entities/ # Entidades de dominio + └── models/ + ├── node.model.ts + ├── order-type.model.ts + └── supply-mode.model.ts +``` + +## 🔧 Personalización + +### Modificar Templates + +Los templates están en la carpeta `templates/`. Cada archivo `.mustache` define cómo se genera un tipo de archivo. + +Templates disponibles: +- `model.mustache` - DTOs +- `model-entity.mustache` - Entidades del modelo +- `mapper.mustache` - Mappers +- `api.repository.contract.mustache` - Contratos de repositorio +- `api.repository.impl.mustache` - Implementaciones de repositorio +- `api.use-cases.contract.mustache` - Contratos de use cases +- `api.use-cases.impl.mustache` - Implementaciones de use cases +- `repository.provider.mustache` - Providers de repositorio +- `use-cases.provider.mustache` - Providers de use cases + +### Variables Mustache Disponibles + +```mustache +{{classname}} - Nombre de la clase (ej: "OrderType") +{{classVarName}} - Nombre en camelCase (ej: "orderType") +{{classFilename}} - Nombre del archivo (ej: "order-type") +{{constantName}} - Constante (ej: "ORDER_TYPE") +{{description}} - Descripción del schema +{{httpMethod}} - Método HTTP (get, post, etc) +{{path}} - Path del endpoint +{{nickname}} - Nombre del método +{{allParams}} - Todos los parámetros +{{returnType}} - Tipo de retorno +{{vars}} - Variables del modelo +``` + +## 📊 Reporte de Generación + +Después de cada generación, se crea un archivo `generation-report.json` con estadísticas: + +```json +{ + "timestamp": "2025-01-15T10:30:00.000Z", + "tags": 3, + "endpoints": 8, + "outputDirectory": "./src/app", + "structure": { + "dtos": 15, + "repositories": 9, + "mappers": 3, + "useCases": 6 + } +} +``` + +## 🎯 Ejemplo Completo + +### 1. Preparar tu proyecto + +```bash +# Clonar o copiar el generador +cd mi-proyecto-angular +mkdir generator +cd generator +# Copiar archivos del generador aquí +``` + +### 2. Copiar tu Swagger + +```bash +cp ../docs/api.yaml ./swagger.yaml +``` + +### 3. Generar código + +```bash +node generate.js +``` + +### 4. Registrar providers en Angular + +En tu `app.module.ts` o `app.config.ts`: + +```typescript +import { NodeRepositoryProvider } from '@/di/repositories/node.repository.provider'; +import { NodeUseCasesProvider } from '@/di/use-cases/node.use-cases.provider'; +// ... importar otros providers + +@NgModule({ + providers: [ + // Repositories + NodeRepositoryProvider, + OrderTypeRepositoryProvider, + SupplyModeRepositoryProvider, + + // Use Cases + NodeUseCasesProvider, + OrderTypeUseCasesProvider, + SupplyModeUseCasesProvider + ] +}) +export class AppModule {} +``` + +### 5. Usar en componentes + +```typescript +import { Component, inject } from '@angular/core'; +import { NODE_USE_CASES, NodeUseCases } from '@/domain/use-cases/node/node.use-cases.contract'; + +@Component({ + selector: 'app-nodes', + template: `...` +}) +export class NodesComponent { + #nodeUseCases = inject(NODE_USE_CASES); + + loadNodes() { + this.#nodeUseCases.getNodes('TI').subscribe(nodes => { + console.log(nodes); + }); + } +} +``` + +## 🐛 Troubleshooting + +### Error: openapi-generator-cli no encontrado + +```bash +npm install -g @openapitools/openapi-generator-cli +# o +npm run setup +``` + +### Error: Archivo swagger.yaml no encontrado + +Asegúrate de especificar la ruta correcta: +```bash +node generate.js -i ./ruta/a/tu/swagger.yaml +``` + +### Los imports no se resuelven (@/ no funciona) + +Configura los path aliases en tu `tsconfig.json`: + +```json +{ + "compilerOptions": { + "paths": { + "@/*": ["src/app/*"], + "@environment": ["src/environments/environment"] + } + } +} +``` + +### Los templates no generan el código esperado + +1. Verifica que tus templates están en `./templates/` +2. Revisa la sintaxis Mustache +3. Usa `--dry-run` para verificar sin generar archivos + +## 📝 Notas + +- El generador crea archivos `.ts`, no los compila +- Los providers deben registrarse manualmente en tu módulo Angular +- Asegúrate de tener configurado `@mercadona/common` o ajusta los imports en los templates +- El generador asume Angular 17+ con inject() function + +## 🤝 Contribuir + +Si encuentras bugs o mejoras, siéntete libre de modificar los templates y el script según tus necesidades. + +## 📄 Licencia + +MIT diff --git a/example-swagger.yaml b/example-swagger.yaml new file mode 100644 index 0000000..5ebe8fb --- /dev/null +++ b/example-swagger.yaml @@ -0,0 +1,150 @@ +openapi: 3.0.1 +info: + title: Example API + description: API de ejemplo para probar el generador + version: 1.0.0 +tags: + - name: User + description: Operaciones de usuario + - name: Product + description: Operaciones de productos +paths: + /v1/users: + get: + tags: + - User + summary: Obtener lista de usuarios + operationId: getUsers + parameters: + - name: search + in: query + required: false + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + post: + tags: + - User + summary: Crear usuario + operationId: createUser + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/UserSchema' + /v1/users/{id}: + get: + tags: + - User + summary: Obtener usuario por ID + operationId: getUserById + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserSchema' + delete: + tags: + - User + summary: Eliminar usuario + operationId: deleteUser + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + /v1/products: + get: + tags: + - Product + summary: Obtener lista de productos + operationId: getProducts + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ProductResponse' +components: + schemas: + UserSchema: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: Juan Pérez + email: + type: string + example: juan@example.com + active: + type: boolean + example: true + CreateUserRequest: + type: object + required: + - name + - email + properties: + name: + type: string + example: Juan Pérez + email: + type: string + example: juan@example.com + UserResponse: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/UserSchema' + ProductSchema: + type: object + properties: + id: + type: integer + example: 100 + name: + type: string + example: Laptop HP + price: + type: number + format: float + example: 599.99 + ProductResponse: + type: object + properties: + products: + type: array + items: + $ref: '#/components/schemas/ProductSchema' diff --git a/generate.js b/generate.js new file mode 100755 index 0000000..707db06 --- /dev/null +++ b/generate.js @@ -0,0 +1,311 @@ +#!/usr/bin/env node + +const { execSync } = require('child_process'); +const fs = require('fs-extra'); +const path = require('path'); +const yaml = require('js-yaml'); +const { program } = require('commander'); + +// Colores para console (sin dependencias externas) +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + blue: '\x1b[34m', + yellow: '\x1b[33m', + red: '\x1b[31m', + cyan: '\x1b[36m' +}; + +function log(message, color = 'reset') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function logSuccess(message) { + log(`✅ ${message}`, 'green'); +} + +function logInfo(message) { + log(`ℹ️ ${message}`, 'blue'); +} + +function logWarning(message) { + log(`⚠️ ${message}`, 'yellow'); +} + +function logError(message) { + log(`❌ ${message}`, 'red'); +} + +function logStep(message) { + log(`\n🚀 ${message}`, 'cyan'); +} + +// Configuración del CLI +program + .name('generate-clean-arch') + .description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger') + .version('1.0.0') + .option('-i, --input ', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml') + .option('-o, --output ', 'Directorio de salida', './src/app') + .option('-t, --templates ', 'Directorio de templates personalizados', './templates') + .option('--skip-install', 'No instalar dependencias') + .option('--dry-run', 'Simular sin generar archivos') + .parse(process.argv); + +const options = program.opts(); + +// Validar que existe openapi-generator-cli +function checkOpenApiGenerator() { + try { + execSync('openapi-generator-cli version', { stdio: 'ignore' }); + return true; + } catch (error) { + return false; + } +} + +// Instalar openapi-generator-cli +function installOpenApiGenerator() { + logStep('Instalando @openapitools/openapi-generator-cli...'); + try { + execSync('npm install -g @openapitools/openapi-generator-cli', { stdio: 'inherit' }); + logSuccess('OpenAPI Generator CLI instalado correctamente'); + } catch (error) { + logError('Error al instalar OpenAPI Generator CLI'); + process.exit(1); + } +} + +// Crear estructura de directorios +function createDirectoryStructure(baseDir) { + const dirs = [ + path.join(baseDir, 'data/dtos'), + path.join(baseDir, 'data/repositories'), + path.join(baseDir, 'data/mappers'), + path.join(baseDir, 'domain/repositories'), + path.join(baseDir, 'domain/use-cases'), + path.join(baseDir, 'di/repositories'), + path.join(baseDir, 'di/use-cases'), + path.join(baseDir, 'entities/models') + ]; + + dirs.forEach(dir => { + fs.ensureDirSync(dir); + }); + + logSuccess('Estructura de directorios creada'); +} + +// Analizar el swagger para extraer tags y dominios +function analyzeSwagger(swaggerFile) { + logStep('Analizando archivo OpenAPI...'); + + try { + const fileContent = fs.readFileSync(swaggerFile, 'utf8'); + const swagger = yaml.load(fileContent); + + const tags = swagger.tags || []; + const paths = swagger.paths || {}; + + logInfo(`Encontrados ${tags.length} tags en el API`); + logInfo(`Encontrados ${Object.keys(paths).length} endpoints`); + + tags.forEach(tag => { + logInfo(` - ${tag.name}: ${tag.description || 'Sin descripción'}`); + }); + + return { tags, paths, swagger }; + } catch (error) { + logError(`Error al leer el archivo Swagger: ${error.message}`); + process.exit(1); + } +} + +// Generar código con OpenAPI Generator +function generateCode(swaggerFile, templatesDir) { + logStep('Generando código desde OpenAPI...'); + + const tempDir = path.join(process.cwd(), '.temp-generated'); + + // Limpiar directorio temporal + if (fs.existsSync(tempDir)) { + fs.removeSync(tempDir); + } + + try { + const command = `openapi-generator-cli generate \ + -i "${swaggerFile}" \ + -g typescript-angular \ + -t "${templatesDir}" \ + -o "${tempDir}" \ + --additional-properties=ngVersion=17.0.0,withInterfaces=true,providedInRoot=false,supportsES6=true,modelPropertyNaming=camelCase`; + + execSync(command, { stdio: 'inherit' }); + logSuccess('Código generado correctamente'); + + return tempDir; + } catch (error) { + logError('Error al generar código'); + if (fs.existsSync(tempDir)) { + fs.removeSync(tempDir); + } + process.exit(1); + } +} + +// Organizar archivos según Clean Architecture +function organizeFiles(tempDir, outputDir) { + logStep('Organizando archivos en estructura Clean Architecture...'); + + const moves = [ + { from: 'model', to: path.join(outputDir, 'data/dtos'), pattern: '*.dto.ts' }, + { from: 'model', to: path.join(outputDir, 'entities/models'), pattern: '*.model.ts' }, + { from: 'api', to: path.join(outputDir, 'domain/repositories'), pattern: '*.repository.contract.ts' }, + { from: 'api', to: path.join(outputDir, 'data/repositories'), pattern: '*.repository.impl.ts' }, + { from: 'api', to: path.join(outputDir, 'data/repositories'), pattern: '*.repository.mock.ts' }, + { from: 'api', to: path.join(outputDir, 'data/mappers'), pattern: '*.mapper.ts' }, + { from: 'api', to: path.join(outputDir, 'domain/use-cases'), pattern: '*.use-cases.contract.ts' }, + { from: 'api', to: path.join(outputDir, 'domain/use-cases'), pattern: '*.use-cases.impl.ts' }, + { from: 'providers', to: path.join(outputDir, 'di/repositories'), pattern: '*.repository.provider.ts' }, + { from: 'providers', to: path.join(outputDir, 'di/use-cases'), pattern: '*.use-cases.provider.ts' } + ]; + + let filesMoved = 0; + + moves.forEach(({ from, to, pattern }) => { + const sourceDir = path.join(tempDir, from); + + if (fs.existsSync(sourceDir)) { + fs.ensureDirSync(to); + + const files = fs.readdirSync(sourceDir).filter(file => { + if (pattern.includes('*')) { + const regex = new RegExp(pattern.replace('*', '.*')); + return regex.test(file); + } + return file.endsWith(pattern); + }); + + files.forEach(file => { + const sourcePath = path.join(sourceDir, file); + const destPath = path.join(to, file); + + fs.copySync(sourcePath, destPath); + filesMoved++; + logInfo(` ${file} → ${path.relative(process.cwd(), destPath)}`); + }); + } + }); + + logSuccess(`${filesMoved} archivos organizados correctamente`); +} + +// Limpiar directorio temporal +function cleanup(tempDir) { + if (fs.existsSync(tempDir)) { + fs.removeSync(tempDir); + logInfo('Archivos temporales eliminados'); + } +} + +// Generar reporte +function generateReport(outputDir, analysis) { + logStep('Generando reporte de generación...'); + + const report = { + timestamp: new Date().toISOString(), + tags: analysis.tags.length, + endpoints: Object.keys(analysis.paths).length, + outputDirectory: outputDir, + structure: { + dtos: fs.readdirSync(path.join(outputDir, 'data/dtos')).length, + repositories: fs.readdirSync(path.join(outputDir, 'data/repositories')).length, + mappers: fs.readdirSync(path.join(outputDir, 'data/mappers')).length, + useCases: fs.readdirSync(path.join(outputDir, 'domain/use-cases')).length + } + }; + + const reportPath = path.join(process.cwd(), 'generation-report.json'); + fs.writeJsonSync(reportPath, report, { spaces: 2 }); + + logSuccess(`Reporte guardado en: ${reportPath}`); + + return report; +} + +// Función principal +async function main() { + console.log('\n' + '='.repeat(60)); + log(' OpenAPI Clean Architecture Generator', 'bright'); + log(' Angular + Clean Architecture Code Generator', 'cyan'); + console.log('='.repeat(60) + '\n'); + + // Validar archivo de entrada + if (!fs.existsSync(options.input)) { + logError(`Archivo no encontrado: ${options.input}`); + process.exit(1); + } + + logInfo(`Archivo de entrada: ${options.input}`); + logInfo(`Directorio de salida: ${options.output}`); + logInfo(`Templates: ${options.templates}`); + + if (options.dryRun) { + logWarning('Modo DRY RUN - No se generarán archivos'); + } + + // Verificar/Instalar OpenAPI Generator + if (!checkOpenApiGenerator()) { + logWarning('OpenAPI Generator CLI no encontrado'); + if (!options.skipInstall) { + installOpenApiGenerator(); + } else { + logError('Instala openapi-generator-cli con: npm install -g @openapitools/openapi-generator-cli'); + process.exit(1); + } + } else { + logSuccess('OpenAPI Generator CLI encontrado'); + } + + // Analizar Swagger + const analysis = analyzeSwagger(options.input); + + if (options.dryRun) { + logInfo('Finalizando en modo DRY RUN'); + return; + } + + // Crear estructura de directorios + createDirectoryStructure(options.output); + + // Generar código + const tempDir = generateCode(options.input, options.templates); + + // Organizar archivos + organizeFiles(tempDir, options.output); + + // Limpiar + cleanup(tempDir); + + // Generar reporte + const report = generateReport(options.output, analysis); + + // Resumen final + console.log('\n' + '='.repeat(60)); + log(' ✨ Generación completada con éxito', 'green'); + console.log('='.repeat(60)); + console.log(`\n📊 Resumen:`); + console.log(` - DTOs generados: ${report.structure.dtos}`); + console.log(` - Repositories: ${report.structure.repositories}`); + console.log(` - Mappers: ${report.structure.mappers}`); + console.log(` - Use Cases: ${report.structure.useCases}`); + console.log(`\n📁 Archivos generados en: ${colors.cyan}${options.output}${colors.reset}\n`); +} + +// Ejecutar +main().catch(error => { + logError(`Error fatal: ${error.message}`); + console.error(error); + process.exit(1); +}); diff --git a/openapitools.json b/openapitools.json new file mode 100644 index 0000000..b98562b --- /dev/null +++ b/openapitools.json @@ -0,0 +1,23 @@ +{ + "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.2.0", + "generators": { + "typescript-angular-clean": { + "generatorName": "typescript-angular", + "output": "./.temp-generated", + "glob": "**/*", + "additionalProperties": { + "ngVersion": "17.0.0", + "modelPropertyNaming": "camelCase", + "supportsES6": true, + "withInterfaces": true, + "providedInRoot": false, + "npmName": "api-client", + "npmVersion": "1.0.0" + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fed694d --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "openapi-clean-arch-generator", + "version": "1.0.0", + "description": "Generador de código Angular con Clean Architecture desde OpenAPI/Swagger", + "main": "generate.js", + "bin": { + "generate-clean-arch": "./generate.js" + }, + "scripts": { + "generate": "node generate.js", + "setup": "npm install -g @openapitools/openapi-generator-cli" + }, + "keywords": [ + "openapi", + "swagger", + "angular", + "clean-architecture", + "code-generator" + ], + "author": "Blas", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^11.1.0", + "fs-extra": "^11.2.0", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/templates/api.repository.contract.mustache b/templates/api.repository.contract.mustache new file mode 100644 index 0000000..ea80935 --- /dev/null +++ b/templates/api.repository.contract.mustache @@ -0,0 +1,34 @@ +{{#apiInfo}} +{{#apis}} +{{#operations}} +import { InjectionToken } from '@angular/core'; +import { Observable } from 'rxjs'; +{{#imports}} +import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; +{{/imports}} + +/** + * {{classname}} Repository Contract + * Generated from OpenAPI tag: {{classname}} + */ +export interface {{classname}}Repository { +{{#operation}} + /** + * {{summary}} + {{#notes}} + * {{notes}} + {{/notes}} + {{#allParams}} + * @param {{paramName}} {{description}} + {{/allParams}} + */ + {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{dataType}}{{^-last}}, {{/-last}}{{/allParams}}): Observable<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}void{{/returnType}}>; + +{{/operation}} +} + +export const {{constantName}}_REPOSITORY = new InjectionToken<{{classname}}Repository>('{{constantName}}_REPOSITORY'); + +{{/operations}} +{{/apis}} +{{/apiInfo}} diff --git a/templates/api.repository.impl.mustache b/templates/api.repository.impl.mustache new file mode 100644 index 0000000..e51764b --- /dev/null +++ b/templates/api.repository.impl.mustache @@ -0,0 +1,61 @@ +{{#apiInfo}} +{{#apis}} +{{#operations}} +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { environment } from '@environment'; + +import { MRepository } from '@mercadona/core/utils/repository'; + +import { {{classname}}Repository } from '../../../domain/repositories/{{classFilename}}.repository.contract'; +{{#imports}} +import { {{classname}}Dto } from '@/dtos/{{classFilename}}/{{classFilename}}.dto'; +import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; +import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}/{{classFilename}}.mapper'; +{{/imports}} + +/** + * {{classname}} Repository Implementation + * Generated from OpenAPI tag: {{classname}} + */ +@Injectable() +export class {{classname}}RepositoryImpl extends MRepository implements {{classname}}Repository { + constructor() { + super(`${environment.modapApi.url}`); + } + +{{#operation}} + {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{dataType}}{{^-last}}, {{/-last}}{{/allParams}}): Observable<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}void{{/returnType}}> { + {{#isListContainer}} + return this.{{httpMethod}}<{{returnBaseType}}Dto>('{{path}}'{{#hasQueryParams}}, { + params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} } + }{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}}) + .pipe( + map((response) => response.{{#vendorExtensions}}{{x-response-property}}{{/vendorExtensions}}{{^vendorExtensions}}items{{/vendorExtensions}}.map({{returnBaseType}}Mapper)) + ); + {{/isListContainer}} + {{^isListContainer}} + {{#returnType}} + return this.{{httpMethod}}<{{returnType}}Dto>('{{path}}'{{#hasQueryParams}}, { + params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} } + }{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}}) + .pipe( + map({{returnType}}Mapper) + ); + {{/returnType}} + {{^returnType}} + return this.{{httpMethod}}('{{path}}'{{#hasQueryParams}}, { + params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} } + }{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}}); + {{/returnType}} + {{/isListContainer}} + } + +{{/operation}} +} + +{{/operations}} +{{/apis}} +{{/apiInfo}} diff --git a/templates/api.use-cases.contract.mustache b/templates/api.use-cases.contract.mustache new file mode 100644 index 0000000..096c8f7 --- /dev/null +++ b/templates/api.use-cases.contract.mustache @@ -0,0 +1,34 @@ +{{#apiInfo}} +{{#apis}} +{{#operations}} +import { InjectionToken } from '@angular/core'; +import { Observable } from 'rxjs'; +{{#imports}} +import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; +{{/imports}} + +/** + * {{classname}} Use Cases Contract + * Generated from OpenAPI tag: {{classname}} + */ +export interface {{classname}}UseCases { +{{#operation}} + /** + * {{summary}} + {{#notes}} + * {{notes}} + {{/notes}} + {{#allParams}} + * @param {{paramName}} {{description}} + {{/allParams}} + */ + {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{dataType}}{{^-last}}, {{/-last}}{{/allParams}}): Observable<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}void{{/returnType}}>; + +{{/operation}} +} + +export const {{constantName}}_USE_CASES = new InjectionToken<{{classname}}UseCases>('{{constantName}}_USE_CASES'); + +{{/operations}} +{{/apis}} +{{/apiInfo}} diff --git a/templates/api.use-cases.impl.mustache b/templates/api.use-cases.impl.mustache new file mode 100644 index 0000000..297770e --- /dev/null +++ b/templates/api.use-cases.impl.mustache @@ -0,0 +1,32 @@ +{{#apiInfo}} +{{#apis}} +{{#operations}} +import { inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { {{classname}}UseCases } from './{{classFilename}}.use-cases.contract'; + +import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract'; +{{#imports}} +import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; +{{/imports}} + +/** + * {{classname}} Use Cases Implementation + * Generated from OpenAPI tag: {{classname}} + */ +@Injectable() +export class {{classname}}UseCasesImpl implements {{classname}}UseCases { + #{{classVarName}}Repository: {{classname}}Repository = inject({{constantName}}_REPOSITORY); + +{{#operation}} + {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{dataType}}{{^-last}}, {{/-last}}{{/allParams}}): Observable<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}void{{/returnType}}> { + return this.#{{classVarName}}Repository.{{nickname}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + } + +{{/operation}} +} + +{{/operations}} +{{/apis}} +{{/apiInfo}} diff --git a/templates/mapper.mustache b/templates/mapper.mustache new file mode 100644 index 0000000..3ea1ce5 --- /dev/null +++ b/templates/mapper.mustache @@ -0,0 +1,28 @@ +{{#apiInfo}} +{{#apis}} +{{#operations}} +import { MapFromFn } from '@mercadona/common/public'; +import { Builder } from '@mercadona/common/utils'; + +import { {{classname}}Dto } from '@/dtos/{{classFilename}}/{{classFilename}}.dto'; +import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; + +/** + * {{classname}} Mapper + * Converts DTO to Domain Entity + * Generated from OpenAPI schema: {{classname}} + */ +export const {{classVarName}}Mapper: MapFromFn<{{classname}}Dto, {{classname}}> = (dto: {{classname}}Dto): {{classname}} => + Builder.forModel({{classname}}) +{{#allModels}} +{{#model}} +{{#vars}} + .{{name}}(dto.{{name}}) +{{/vars}} +{{/model}} +{{/allModels}} + .build(); + +{{/operations}} +{{/apis}} +{{/apiInfo}} diff --git a/templates/model-entity.mustache b/templates/model-entity.mustache new file mode 100644 index 0000000..dc227f3 --- /dev/null +++ b/templates/model-entity.mustache @@ -0,0 +1,24 @@ +{{#models}} +{{#model}} +{{#imports}} +import { {{classname}} } from './{{classFilename}}.model'; +{{/imports}} + +/** + * {{classname}} Entity + * {{#description}}{{description}}{{/description}} + * Generated from OpenAPI schema + */ +export class {{classname}} { +{{#vars}} +{{#description}} + /** + * {{description}} + */ +{{/description}} + {{name}}{{^required}}?{{/required}}: {{dataType}}; +{{/vars}} +} + +{{/model}} +{{/models}} diff --git a/templates/model.mustache b/templates/model.mustache new file mode 100644 index 0000000..ee2523b --- /dev/null +++ b/templates/model.mustache @@ -0,0 +1,20 @@ +{{#models}} +{{#model}} +/** + * {{classname}} DTO + * {{#description}}{{description}}{{/description}} + * Generated from OpenAPI specification + */ +export interface {{classname}}Dto { +{{#vars}} +{{#description}} + /** + * {{description}} + */ +{{/description}} + {{name}}{{^required}}?{{/required}}: {{dataType}}; +{{/vars}} +} + +{{/model}} +{{/models}} diff --git a/templates/repository.provider.mustache b/templates/repository.provider.mustache new file mode 100644 index 0000000..df943ce --- /dev/null +++ b/templates/repository.provider.mustache @@ -0,0 +1,20 @@ +{{#apiInfo}} +{{#apis}} +{{#operations}} +import { Provider } from '@angular/core'; + +import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract'; +import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl'; + +/** + * {{classname}} Repository Provider + * Binds the repository contract with its implementation + */ +export const {{classname}}RepositoryProvider: Provider = { + provide: {{constantName}}_REPOSITORY, + useClass: {{classname}}RepositoryImpl +}; + +{{/operations}} +{{/apis}} +{{/apiInfo}} diff --git a/templates/use-cases.provider.mustache b/templates/use-cases.provider.mustache new file mode 100644 index 0000000..f89c7ab --- /dev/null +++ b/templates/use-cases.provider.mustache @@ -0,0 +1,20 @@ +{{#apiInfo}} +{{#apis}} +{{#operations}} +import { Provider } from '@angular/core'; + +import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract'; +import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl'; + +/** + * {{classname}} Use Cases Provider + * Binds the use cases contract with its implementation + */ +export const {{classname}}UseCasesProvider: Provider = { + provide: {{constantName}}_USE_CASES, + useClass: {{classname}}UseCasesImpl +}; + +{{/operations}} +{{/apis}} +{{/apiInfo}}