diff --git a/src/generators/clean-arch.generator.ts b/src/generators/clean-arch.generator.ts index b4b34f7..17c09f2 100644 --- a/src/generators/clean-arch.generator.ts +++ b/src/generators/clean-arch.generator.ts @@ -3,6 +3,7 @@ import path from 'path'; import mustache from 'mustache'; import { logStep, logSuccess, logInfo } from '../utils/logger'; import { mapSwaggerTypeToTs } from '../utils/type-mapper'; +import { toCamelCase } from '../utils/name-formatter'; import type { SwaggerAnalysis, OpenApiSchema, @@ -43,7 +44,7 @@ export function generateCleanArchitecture( if (rawProperties[k].$ref) { tsType = rawProperties[k].$ref.split('/').pop() || 'unknown'; } else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) { - tsType = `Array<${rawProperties[k].items.$ref.split('/').pop()}>`; + tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`; } return { name: k, @@ -53,14 +54,28 @@ export function generateCleanArchitecture( }; }); + // Collect imports for types referenced via $ref in properties + const referencedTypes = new Set(); + Object.values(rawProperties).forEach((prop) => { + if (prop.$ref) { + referencedTypes.add(prop.$ref.split('/').pop() || ''); + } else if (prop.type === 'array' && prop.items?.$ref) { + referencedTypes.add(prop.items.$ref.split('/').pop() || ''); + } + }); + const modelImports = [...referencedTypes] + .filter(Boolean) + .map((name) => ({ classname: name, classFilename: toCamelCase(name) })); + const modelViewData = { models: [ { model: { classname: baseName, - classFilename: baseName.toLowerCase(), - classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1), + classFilename: toCamelCase(baseName), + classVarName: toCamelCase(baseName), description: schemaObj.description || '', + imports: modelImports, vars: varsMap } } @@ -75,8 +90,8 @@ export function generateCleanArchitecture( { operations: { classname: baseName, - classFilename: baseName.toLowerCase(), - classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1) + classFilename: toCamelCase(baseName), + classVarName: toCamelCase(baseName) } } ] @@ -88,14 +103,10 @@ export function generateCleanArchitecture( if (fs.existsSync(modelTemplatePath)) { const template = fs.readFileSync(modelTemplatePath, 'utf8'); const output = mustache.render(template, modelViewData); - const destPath = path.join( - outputDir, - 'entities/models', - `${baseName.toLowerCase()}.model.ts` - ); + const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`); fs.writeFileSync(destPath, output); generatedCount.models++; - logInfo(` ${baseName.toLowerCase()}.model.ts → ${path.relative(process.cwd(), destPath)}`); + logInfo(` ${toCamelCase(baseName)}.model.ts → ${path.relative(process.cwd(), destPath)}`); } // Mapper @@ -103,7 +114,7 @@ export function generateCleanArchitecture( if (fs.existsSync(mapperTemplatePath)) { const template = fs.readFileSync(mapperTemplatePath, 'utf8'); const output = mustache.render(template, mapperViewData); - const destPath = path.join(outputDir, 'data/mappers', `${baseName.toLowerCase()}.mapper.ts`); + const destPath = path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.ts`); fs.writeFileSync(destPath, output); generatedCount.mappers++; } @@ -152,7 +163,7 @@ export function generateCleanArchitecture( returnBaseType = returnType; } else if (responseSchema.type === 'array' && responseSchema.items?.$ref) { returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown'; - returnType = `Array<${returnBaseType}>`; + returnType = `${returnBaseType}[]`; isListContainer = true; } } @@ -178,6 +189,8 @@ export function generateCleanArchitecture( bodyParam: 'body', returnType: returnType !== 'void' ? returnType : false, returnBaseType: returnBaseType !== 'void' ? returnBaseType : false, + returnTypeVarName: returnType !== 'void' ? toCamelCase(returnType) : false, + returnBaseTypeVarName: returnBaseType !== 'void' ? toCamelCase(returnBaseType) : false, isListContainer: isListContainer, vendorExtensions: {} }); @@ -187,14 +200,24 @@ export function generateCleanArchitecture( // Generar por cada Tag Object.keys(tagsMap).forEach((tag) => { - const imports: { classname: string; classFilename: string; classVarName: string }[] = []; + const returnImports: { classname: string; classFilename: string; classVarName: string }[] = []; + const paramImports: { classname: string; classFilename: string; classVarName: string }[] = []; + Object.keys(schemas).forEach((s) => { - if (tagsMap[tag].some((op) => op.returnType === s || op.returnType === `Array<${s}>`)) { - imports.push({ - classname: s, - classFilename: s.toLowerCase(), - classVarName: s.charAt(0).toLowerCase() + s.slice(1) - }); + const usedAsReturn = tagsMap[tag].some( + (op) => op.returnType === s || op.returnType === `${s}[]` + ); + const usedAsParam = tagsMap[tag].some((op) => + op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`) + ); + + const entry = { classname: s, classFilename: toCamelCase(s), classVarName: toCamelCase(s) }; + + if (usedAsReturn) { + returnImports.push(entry); + } else if (usedAsParam) { + // Param-only types: entity import needed for method signatures, but no Dto or Mapper + paramImports.push(entry); } }); @@ -204,10 +227,16 @@ export function generateCleanArchitecture( { operations: { classname: tag, - classFilename: tag.toLowerCase(), + classFilename: toCamelCase(tag), + classVarName: toCamelCase(tag), constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'), operation: tagsMap[tag], - imports: imports + // All entity imports (return + param) — for contracts and use-cases + imports: [...returnImports, ...paramImports], + // Return-type-only imports — for repo impl (Dto + Entity + Mapper) + returnImports, + // Param-only imports — for repo impl (Entity only, no Dto/Mapper) + paramImports } } ] @@ -218,7 +247,7 @@ export function generateCleanArchitecture( templatesDir, 'api.use-cases.contract.mustache', apiViewData, - path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.contract.ts`), + path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.contract.ts`), generatedCount, 'useCases' ); @@ -227,7 +256,7 @@ export function generateCleanArchitecture( templatesDir, 'api.use-cases.impl.mustache', apiViewData, - path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.impl.ts`), + path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.ts`), generatedCount, 'useCases' ); @@ -236,7 +265,7 @@ export function generateCleanArchitecture( templatesDir, 'api.repository.contract.mustache', apiViewData, - path.join(outputDir, 'domain/repositories', `${tag.toLowerCase()}.repository.contract.ts`), + path.join(outputDir, 'domain/repositories', `${toCamelCase(tag)}.repository.contract.ts`), generatedCount, 'repositories' ); @@ -245,7 +274,7 @@ export function generateCleanArchitecture( templatesDir, 'api.repository.impl.mustache', apiViewData, - path.join(outputDir, 'data/repositories', `${tag.toLowerCase()}.repository.impl.ts`), + path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.ts`), generatedCount, 'repositories' ); @@ -254,7 +283,7 @@ export function generateCleanArchitecture( templatesDir, 'use-cases.provider.mustache', apiViewData, - path.join(outputDir, 'di/use-cases', `${tag.toLowerCase()}.use-cases.provider.ts`), + path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.ts`), generatedCount, 'providers' ); @@ -263,7 +292,7 @@ export function generateCleanArchitecture( templatesDir, 'repository.provider.mustache', apiViewData, - path.join(outputDir, 'di/repositories', `${tag.toLowerCase()}.repository.provider.ts`), + path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.ts`), generatedCount, 'providers' ); diff --git a/src/generators/dto.generator.ts b/src/generators/dto.generator.ts index d52d18d..83a7747 100644 --- a/src/generators/dto.generator.ts +++ b/src/generators/dto.generator.ts @@ -61,7 +61,7 @@ export function organizeFiles(tempDir: string, outputDir: string): void { logSuccess(`${filesMoved} DTOs movidos correctamente`); } -/** Post-procesa los DTOs generados añadiendo los imports de tipos referenciados. */ +/** Post-procesa los DTOs generados añadiendo imports y normalizando Array → T[]. */ export function addDtoImports(outputDir: string): void { logStep('Añadiendo imports a los DTOs generados...'); @@ -85,11 +85,15 @@ export function addDtoImports(outputDir: string): void { files.forEach((file) => { const filePath = path.join(dtosDir, file); - let content = fs.readFileSync(filePath, 'utf8'); + const originalContent = fs.readFileSync(filePath, 'utf8'); + let content = originalContent; const selfMatch = content.match(/export interface (\w+)/); const selfName = selfMatch ? selfMatch[1] : ''; + // Normalize Array → T[] (openapi-generator-cli always outputs Array) + content = content.replace(/Array<(\w+)>/g, '$1[]'); + // Find all Dto type references in the file body (excluding the interface name itself) const references = new Set(); const typeRegex = /\b(\w+Dto)\b/g; @@ -100,8 +104,6 @@ export function addDtoImports(outputDir: string): void { } } - if (references.size === 0) return; - // Build import lines for each referenced type that exists in the dtoMap const imports: string[] = []; references.forEach((ref) => { @@ -112,9 +114,12 @@ export function addDtoImports(outputDir: string): void { if (imports.length > 0) { content = imports.join('\n') + '\n' + content; + } + + if (content !== originalContent) { fs.writeFileSync(filePath, content); filesProcessed++; - logInfo(` Imports añadidos a ${file}`); + logInfo(` Procesado ${file}`); } }); diff --git a/src/types/openapi.types.ts b/src/types/openapi.types.ts index 9b59ce6..1ff7a85 100644 --- a/src/types/openapi.types.ts +++ b/src/types/openapi.types.ts @@ -65,19 +65,29 @@ export interface OpenApiOperation { * Operación normalizada y lista para ser consumida por los templates Mustache. * Cada instancia representa un endpoint agrupado bajo un tag del API. */ +export interface TagOperationParam { + paramName: string; + dataType: string; + description: string; + required: boolean; + '-last': boolean; +} + export interface TagOperation { nickname: string; summary: string; notes: string; httpMethod: string; path: string; - allParams: unknown[]; + allParams: TagOperationParam[]; hasQueryParams: boolean; queryParams: unknown[]; hasBodyParam: boolean; bodyParam: string; returnType: string | boolean; returnBaseType: string | boolean; + returnTypeVarName: string | boolean; + returnBaseTypeVarName: string | boolean; isListContainer: boolean; vendorExtensions: Record; } diff --git a/src/utils/name-formatter.ts b/src/utils/name-formatter.ts new file mode 100644 index 0000000..c6ff94b --- /dev/null +++ b/src/utils/name-formatter.ts @@ -0,0 +1,12 @@ +/** + * Converts a PascalCase name to camelCase by lowercasing the first character. + * Used to derive class filenames and variable names from schema/tag names. + * + * @example + * toCamelCase('ProductResponse') // 'productResponse' + * toCamelCase('UserSchema') // 'userSchema' + */ +export function toCamelCase(name: string): string { + if (!name) return name; + return name.charAt(0).toLowerCase() + name.slice(1); +} diff --git a/src/utils/type-mapper.ts b/src/utils/type-mapper.ts index f128eb2..b6529b1 100644 --- a/src/utils/type-mapper.ts +++ b/src/utils/type-mapper.ts @@ -7,7 +7,7 @@ export function mapSwaggerTypeToTs(type?: string): string { string: 'string', boolean: 'boolean', number: 'number', - array: 'Array', + array: 'unknown[]', object: 'unknown' }; return typeMap[type] || 'unknown'; diff --git a/templates/api.repository.impl.mustache b/templates/api.repository.impl.mustache index e02ce0d..80bc23b 100644 --- a/templates/api.repository.impl.mustache +++ b/templates/api.repository.impl.mustache @@ -9,12 +9,15 @@ 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}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract'; +{{#returnImports}} +import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto'; import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; -import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}/{{classFilename}}.mapper'; -{{/imports}} +import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper'; +{{/returnImports}} +{{#paramImports}} +import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; +{{/paramImports}} /** * {{classname}} Repository Implementation @@ -33,7 +36,7 @@ export class {{classname}}RepositoryImpl extends MRepository implements {{classn 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)) + map((response) => response.{{#vendorExtensions}}{{x-response-property}}{{/vendorExtensions}}{{^vendorExtensions}}items{{/vendorExtensions}}.map({{{returnBaseTypeVarName}}}Mapper)) ); {{/isListContainer}} {{^isListContainer}} @@ -42,7 +45,7 @@ export class {{classname}}RepositoryImpl extends MRepository implements {{classn params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} } }{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}}) .pipe( - map({{{returnType}}}Mapper) + map({{{returnTypeVarName}}}Mapper) ); {{/returnType}} {{^returnType}} diff --git a/templates/mapper.mustache b/templates/mapper.mustache index 3ea1ce5..418ed34 100644 --- a/templates/mapper.mustache +++ b/templates/mapper.mustache @@ -4,7 +4,7 @@ import { MapFromFn } from '@mercadona/common/public'; import { Builder } from '@mercadona/common/utils'; -import { {{classname}}Dto } from '@/dtos/{{classFilename}}/{{classFilename}}.dto'; +import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto'; import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; /** diff --git a/templates/use-cases.provider.mustache b/templates/use-cases.provider.mustache index 19ed974..58a4f0b 100644 --- a/templates/use-cases.provider.mustache +++ b/templates/use-cases.provider.mustache @@ -3,8 +3,8 @@ {{#operations}} import { EnvironmentProviders, makeEnvironmentProviders } 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'; +import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract'; +import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl'; /** * {{classname}} Use Cases Provider