fix: enhance DTO generation, schema validation, and formatting logic #71
45
main.ts
45
main.ts
@@ -20,11 +20,14 @@ import { analyzeSwagger } from './src/swagger/analyzer';
|
|||||||
import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator';
|
import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator';
|
||||||
import {
|
import {
|
||||||
generateCleanArchitecture,
|
generateCleanArchitecture,
|
||||||
extractTagsWithOperations
|
extractTagsWithOperations,
|
||||||
|
buildTagsMapFromAnalysis,
|
||||||
|
buildSchemaTagMap
|
||||||
} from './src/generators/clean-arch.generator';
|
} from './src/generators/clean-arch.generator';
|
||||||
import { generateReport } from './src/generators/report.generator';
|
import { generateReport } from './src/generators/report.generator';
|
||||||
import { lintGeneratedFiles } from './src/generators/lint.generator';
|
import { lintGeneratedFiles } from './src/generators/lint.generator';
|
||||||
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
|
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
|
||||||
|
import { getExampleMismatches, clearExampleMismatches } from './src/utils/example-validator';
|
||||||
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
|
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
|
||||||
import {
|
import {
|
||||||
loadConfig,
|
loadConfig,
|
||||||
@@ -144,6 +147,7 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createDirectoryStructure(options.output);
|
createDirectoryStructure(options.output);
|
||||||
|
clearExampleMismatches();
|
||||||
|
|
||||||
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
|
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
|
||||||
let selectionFilter: SelectionFilter = {};
|
let selectionFilter: SelectionFilter = {};
|
||||||
@@ -193,17 +197,47 @@ async function main(): Promise<void> {
|
|||||||
// ──────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const tempDir = generateCode(options.input, options.templates);
|
const tempDir = generateCode(options.input, options.templates);
|
||||||
organizeFiles(tempDir, options.output);
|
|
||||||
|
// Compute schema→tag map before organizeFiles so DTOs land in the right subfolder
|
||||||
|
const tagsMapForSchema = buildTagsMapFromAnalysis(analysis, selectionFilter);
|
||||||
|
const schemaTagMap = buildSchemaTagMap(
|
||||||
|
(analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
|
||||||
|
?.schemas || {},
|
||||||
|
tagsMapForSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
organizeFiles(tempDir, options.output, schemaTagMap);
|
||||||
addDtoImports(options.output);
|
addDtoImports(options.output);
|
||||||
generateCleanArchitecture(
|
generateCleanArchitecture(
|
||||||
analysis,
|
analysis,
|
||||||
options.output,
|
options.output,
|
||||||
options.templates,
|
options.templates,
|
||||||
tagApiKeyMap,
|
tagApiKeyMap,
|
||||||
selectionFilter
|
selectionFilter,
|
||||||
|
schemaTagMap
|
||||||
);
|
);
|
||||||
cleanup(tempDir);
|
cleanup(tempDir);
|
||||||
|
|
||||||
|
// ── EXAMPLE/TYPE MISMATCH WARNINGS ─────────────────────────────────────────
|
||||||
|
const mismatches = getExampleMismatches();
|
||||||
|
if (mismatches.length > 0) {
|
||||||
|
console.log('');
|
||||||
|
logWarning(`${mismatches.length} example/type mismatch(es) detected in OpenAPI schemas:`);
|
||||||
|
for (const m of mismatches) {
|
||||||
|
const action =
|
||||||
|
m.action === 'coerced'
|
||||||
|
? `→ coerced to ${JSON.stringify(m.coercedValue)}`
|
||||||
|
: '→ example ignored, using type default';
|
||||||
|
logWarning(
|
||||||
|
` ${m.schemaName}.${m.propertyName}: type '${m.declaredType}' but example is ${m.exampleJsType} (${JSON.stringify(m.exampleValue)}) ${action}`
|
||||||
|
);
|
||||||
|
logDetail(
|
||||||
|
'VALIDATE',
|
||||||
|
`${m.schemaName}.${m.propertyName}: declared=${m.declaredType} example=${JSON.stringify(m.exampleValue)} (${m.exampleJsType}) action=${m.action}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const noLintResult: LintResult = {
|
const noLintResult: LintResult = {
|
||||||
prettier: { ran: false, filesFormatted: 0 },
|
prettier: { ran: false, filesFormatted: 0 },
|
||||||
eslint: { ran: false, filesFixed: 0 }
|
eslint: { ran: false, filesFixed: 0 }
|
||||||
@@ -222,6 +256,11 @@ async function main(): Promise<void> {
|
|||||||
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(` - Mocks: ${report.structure.mocks}`);
|
||||||
|
if (report.warnings.total > 0) {
|
||||||
|
console.log(
|
||||||
|
`\n ${colors.yellow}⚠️ ${report.warnings.total} example/type mismatch(es) (see above)${colors.reset}`
|
||||||
|
);
|
||||||
|
}
|
||||||
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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from 'path';
|
|||||||
import mustache from 'mustache';
|
import mustache from 'mustache';
|
||||||
import { logStep, logSuccess, logDetail } from '../utils/logger';
|
import { logStep, logSuccess, logDetail } from '../utils/logger';
|
||||||
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
|
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
|
||||||
import { toCamelCase } from '../utils/name-formatter';
|
import { toCamelCase, toPascalCase, safePropertyName } from '../utils/name-formatter';
|
||||||
import { resolveMockValue } from '../utils/mock-value-resolver';
|
import { resolveMockValue } from '../utils/mock-value-resolver';
|
||||||
import type {
|
import type {
|
||||||
SwaggerAnalysis,
|
SwaggerAnalysis,
|
||||||
@@ -59,181 +59,14 @@ export function extractTagsWithOperations(analysis: SwaggerAnalysis): TagSummary
|
|||||||
return [...map.values()];
|
return [...map.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
|
/**
|
||||||
export function generateCleanArchitecture(
|
* Builds and returns the tagsMap from the swagger analysis, applying the optional selection filter.
|
||||||
|
* Exported so callers (e.g. main.ts) can compute it before organizeFiles runs.
|
||||||
|
*/
|
||||||
|
export function buildTagsMapFromAnalysis(
|
||||||
analysis: SwaggerAnalysis,
|
analysis: SwaggerAnalysis,
|
||||||
outputDir: string,
|
|
||||||
templatesDir: string,
|
|
||||||
tagApiKeyMap: Record<string, string> = {},
|
|
||||||
selectionFilter: SelectionFilter = {}
|
selectionFilter: SelectionFilter = {}
|
||||||
): GeneratedCount {
|
): Record<string, TagOperation[]> {
|
||||||
logStep('Generating Clean Architecture artefacts using Mustache...');
|
|
||||||
const generatedCount: GeneratedCount = {
|
|
||||||
models: 0,
|
|
||||||
repositories: 0,
|
|
||||||
mappers: 0,
|
|
||||||
useCases: 0,
|
|
||||||
providers: 0,
|
|
||||||
mocks: 0,
|
|
||||||
specs: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const schemas =
|
|
||||||
(analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
|
|
||||||
?.schemas || {};
|
|
||||||
|
|
||||||
// 1. Generate Models, Entities and Mappers from Schemas
|
|
||||||
Object.keys(schemas).forEach((schemaName) => {
|
|
||||||
const baseName = schemaName.replace(/Dto$/, '');
|
|
||||||
|
|
||||||
const schemaObj = schemas[schemaName] as OpenApiSchema;
|
|
||||||
const rawProperties = schemaObj.properties || {};
|
|
||||||
const requiredProps: string[] = schemaObj.required || [];
|
|
||||||
|
|
||||||
const varsMap = Object.keys(rawProperties).map((k) => {
|
|
||||||
let tsType = mapSwaggerTypeToTs(rawProperties[k].type);
|
|
||||||
if (rawProperties[k].$ref) {
|
|
||||||
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
|
|
||||||
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
|
|
||||||
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: k,
|
|
||||||
dataType: tsType,
|
|
||||||
description: rawProperties[k].description || '',
|
|
||||||
required: requiredProps.includes(k)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collect imports for types referenced via $ref in properties
|
|
||||||
const referencedTypes = new Set<string>();
|
|
||||||
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: toCamelCase(baseName),
|
|
||||||
classVarName: toCamelCase(baseName),
|
|
||||||
description: schemaObj.description || '',
|
|
||||||
imports: modelImports,
|
|
||||||
vars: varsMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
allModels: [{ model: { vars: varsMap } }]
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapperViewData = {
|
|
||||||
...modelViewData,
|
|
||||||
apiInfo: {
|
|
||||||
apis: [
|
|
||||||
{
|
|
||||||
operations: {
|
|
||||||
classname: baseName,
|
|
||||||
classFilename: toCamelCase(baseName),
|
|
||||||
classVarName: toCamelCase(baseName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Model (Entities)
|
|
||||||
const modelTemplatePath = path.join(templatesDir, 'model-entity.mustache');
|
|
||||||
if (fs.existsSync(modelTemplatePath)) {
|
|
||||||
const template = fs.readFileSync(modelTemplatePath, 'utf8');
|
|
||||||
const output = mustache.render(template, modelViewData);
|
|
||||||
const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`);
|
|
||||||
fs.writeFileSync(destPath, output);
|
|
||||||
generatedCount.models++;
|
|
||||||
logDetail('generate', `model-entity → ${path.relative(process.cwd(), destPath)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mapper
|
|
||||||
const mapperTemplatePath = path.join(templatesDir, 'mapper.mustache');
|
|
||||||
if (fs.existsSync(mapperTemplatePath)) {
|
|
||||||
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
|
|
||||||
const output = mustache.render(template, mapperViewData);
|
|
||||||
const destPath = path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.ts`);
|
|
||||||
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'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Model spec
|
|
||||||
renderTemplate(
|
|
||||||
templatesDir,
|
|
||||||
'model-entity.spec.mustache',
|
|
||||||
modelViewData,
|
|
||||||
path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.spec.ts`),
|
|
||||||
generatedCount,
|
|
||||||
'specs'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mapper spec
|
|
||||||
renderTemplate(
|
|
||||||
templatesDir,
|
|
||||||
'mapper.spec.mustache',
|
|
||||||
mapperViewData,
|
|
||||||
path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.spec.ts`),
|
|
||||||
generatedCount,
|
|
||||||
'specs'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Generate Use Cases and Repositories from Paths/Tags
|
|
||||||
const tagsMap: Record<string, TagOperation[]> = {};
|
const tagsMap: Record<string, TagOperation[]> = {};
|
||||||
|
|
||||||
Object.keys(analysis.paths).forEach((pathKey) => {
|
Object.keys(analysis.paths).forEach((pathKey) => {
|
||||||
@@ -290,6 +123,12 @@ export function generateCleanArchitecture(
|
|||||||
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
|
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
|
||||||
const hasBodyParam = !!op.requestBody;
|
const hasBodyParam = !!op.requestBody;
|
||||||
|
|
||||||
|
// Sort: required params first, optional params last (TypeScript requirement)
|
||||||
|
allParams.sort((a, b) => {
|
||||||
|
if (a.required === b.required) return 0;
|
||||||
|
return a.required ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
tagsMap[tag].push({
|
tagsMap[tag].push({
|
||||||
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
||||||
summary: op.summary || '',
|
summary: op.summary || '',
|
||||||
@@ -334,10 +173,276 @@ export function generateCleanArchitecture(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return tagsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps each schema basename to the tag subfolder it belongs to.
|
||||||
|
* Schemas used by exactly one tag → that tag's camelCase name.
|
||||||
|
* Schemas used by 0 or multiple tags → 'shared'.
|
||||||
|
*/
|
||||||
|
export function buildSchemaTagMap(
|
||||||
|
schemas: Record<string, unknown>,
|
||||||
|
tagsMap: Record<string, TagOperation[]>
|
||||||
|
): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
Object.keys(schemas).forEach((schemaName) => {
|
||||||
|
const baseName = schemaName.replace(/Dto$/, '');
|
||||||
|
const tagsUsing: string[] = [];
|
||||||
|
Object.keys(tagsMap).forEach((tag) => {
|
||||||
|
const used = tagsMap[tag].some(
|
||||||
|
(op) =>
|
||||||
|
op.returnType === baseName ||
|
||||||
|
op.returnType === `${baseName}[]` ||
|
||||||
|
op.allParams.some((p) => p.dataType === baseName || p.dataType === `${baseName}[]`)
|
||||||
|
);
|
||||||
|
if (used) tagsUsing.push(tag);
|
||||||
|
});
|
||||||
|
result[baseName] = tagsUsing.length === 1 ? toCamelCase(tagsUsing[0]) : 'shared';
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
|
||||||
|
export function generateCleanArchitecture(
|
||||||
|
analysis: SwaggerAnalysis,
|
||||||
|
outputDir: string,
|
||||||
|
templatesDir: string,
|
||||||
|
tagApiKeyMap: Record<string, string> = {},
|
||||||
|
selectionFilter: SelectionFilter = {},
|
||||||
|
precomputedSchemaTagMap: Record<string, string> = {}
|
||||||
|
): GeneratedCount {
|
||||||
|
logStep('Generating Clean Architecture artefacts using Mustache...');
|
||||||
|
const generatedCount: GeneratedCount = {
|
||||||
|
models: 0,
|
||||||
|
repositories: 0,
|
||||||
|
mappers: 0,
|
||||||
|
useCases: 0,
|
||||||
|
providers: 0,
|
||||||
|
mocks: 0,
|
||||||
|
specs: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const schemas =
|
||||||
|
(analysis.swagger as { components?: { schemas?: Record<string, unknown> } }).components
|
||||||
|
?.schemas || {};
|
||||||
|
|
||||||
|
// Build tagsMap first — needed to compute schemaTagMap before the schema loop
|
||||||
|
const tagsMap = buildTagsMapFromAnalysis(analysis, selectionFilter);
|
||||||
|
|
||||||
|
// Map each schema basename → tag subfolder ('shared' if used by 0 or >1 tags)
|
||||||
|
const schemaTagMap =
|
||||||
|
Object.keys(precomputedSchemaTagMap).length > 0
|
||||||
|
? precomputedSchemaTagMap
|
||||||
|
: buildSchemaTagMap(schemas, tagsMap);
|
||||||
|
|
||||||
|
// 1. Generate Models, Entities and Mappers from Schemas
|
||||||
|
Object.keys(schemas).forEach((schemaName) => {
|
||||||
|
const baseName = schemaName.replace(/Dto$/, '');
|
||||||
|
const tagFilename = schemaTagMap[baseName] || 'shared';
|
||||||
|
|
||||||
|
const schemaObj = schemas[schemaName] as OpenApiSchema;
|
||||||
|
const rawProperties = schemaObj.properties || {};
|
||||||
|
const requiredProps: string[] = schemaObj.required || [];
|
||||||
|
|
||||||
|
const varsMap = Object.keys(rawProperties).map((k) => {
|
||||||
|
let tsType = mapSwaggerTypeToTs(rawProperties[k].type);
|
||||||
|
const isInlineObject = rawProperties[k].type === 'object' && !rawProperties[k].$ref;
|
||||||
|
if (rawProperties[k].$ref) {
|
||||||
|
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
|
||||||
|
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
|
||||||
|
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: safePropertyName(k),
|
||||||
|
originalName: k,
|
||||||
|
dataType: tsType,
|
||||||
|
description: rawProperties[k].description || '',
|
||||||
|
required: requiredProps.includes(k),
|
||||||
|
hasMockValue: !isInlineObject
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect imports for types referenced via $ref in properties
|
||||||
|
const referencedTypes = new Set<string>();
|
||||||
|
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),
|
||||||
|
tagFilename: schemaTagMap[name] || 'shared'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const modelViewData = {
|
||||||
|
tagFilename,
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
model: {
|
||||||
|
classname: baseName,
|
||||||
|
classFilename: toCamelCase(baseName),
|
||||||
|
classVarName: toCamelCase(baseName),
|
||||||
|
description: schemaObj.description || '',
|
||||||
|
imports: modelImports,
|
||||||
|
vars: varsMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
allModels: [{ model: { vars: varsMap } }]
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapperViewData = {
|
||||||
|
...modelViewData,
|
||||||
|
apiInfo: {
|
||||||
|
apis: [
|
||||||
|
{
|
||||||
|
operations: {
|
||||||
|
classname: baseName,
|
||||||
|
classFilename: toCamelCase(baseName),
|
||||||
|
classVarName: toCamelCase(baseName),
|
||||||
|
tagFilename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Model (Entities)
|
||||||
|
const modelTemplatePath = path.join(templatesDir, 'model-entity.mustache');
|
||||||
|
if (fs.existsSync(modelTemplatePath)) {
|
||||||
|
const template = fs.readFileSync(modelTemplatePath, 'utf8');
|
||||||
|
const output = mustache.render(template, modelViewData);
|
||||||
|
const destPath = path.join(
|
||||||
|
outputDir,
|
||||||
|
'entities/models',
|
||||||
|
tagFilename,
|
||||||
|
`${toCamelCase(baseName)}.model.ts`
|
||||||
|
);
|
||||||
|
fs.ensureDirSync(path.dirname(destPath));
|
||||||
|
fs.writeFileSync(destPath, output);
|
||||||
|
generatedCount.models++;
|
||||||
|
logDetail('generate', `model-entity → ${path.relative(process.cwd(), destPath)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapper
|
||||||
|
const mapperTemplatePath = path.join(templatesDir, 'mapper.mustache');
|
||||||
|
if (fs.existsSync(mapperTemplatePath)) {
|
||||||
|
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
|
||||||
|
const output = mustache.render(template, mapperViewData);
|
||||||
|
const destPath = path.join(
|
||||||
|
outputDir,
|
||||||
|
'data/mappers',
|
||||||
|
tagFilename,
|
||||||
|
`${toCamelCase(baseName)}.mapper.ts`
|
||||||
|
);
|
||||||
|
fs.ensureDirSync(path.dirname(destPath));
|
||||||
|
fs.writeFileSync(destPath, output);
|
||||||
|
generatedCount.mappers++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO mock — values resolved from raw schema (example, format, type)
|
||||||
|
const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
|
||||||
|
name: safePropertyName(k),
|
||||||
|
mockValue: resolveMockValue(k, rawProperties[k], 'dto', schemaName)
|
||||||
|
}));
|
||||||
|
const dtoMockImports = [...referencedTypes].filter(Boolean).map((name) => {
|
||||||
|
const targetTag = schemaTagMap[name] || 'shared';
|
||||||
|
const targetFile = `${toCamelCase(name)}.dto.mock`;
|
||||||
|
const importPath =
|
||||||
|
targetTag === tagFilename ? `./${targetFile}` : `../${targetTag}/${targetFile}`;
|
||||||
|
return {
|
||||||
|
classname: name,
|
||||||
|
classFilename: toCamelCase(name),
|
||||||
|
tagFilename: targetTag,
|
||||||
|
importPath
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dtoMockViewData = {
|
||||||
|
tagFilename,
|
||||||
|
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', tagFilename, `${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',
|
||||||
|
tagFilename,
|
||||||
|
`${toCamelCase(baseName)}.model.mock.ts`
|
||||||
|
),
|
||||||
|
generatedCount,
|
||||||
|
'mocks'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Model spec
|
||||||
|
renderTemplate(
|
||||||
|
templatesDir,
|
||||||
|
'model-entity.spec.mustache',
|
||||||
|
modelViewData,
|
||||||
|
path.join(
|
||||||
|
outputDir,
|
||||||
|
'entities/models',
|
||||||
|
tagFilename,
|
||||||
|
`${toCamelCase(baseName)}.model.spec.ts`
|
||||||
|
),
|
||||||
|
generatedCount,
|
||||||
|
'specs'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mapper spec
|
||||||
|
renderTemplate(
|
||||||
|
templatesDir,
|
||||||
|
'mapper.spec.mustache',
|
||||||
|
mapperViewData,
|
||||||
|
path.join(outputDir, 'data/mappers', tagFilename, `${toCamelCase(baseName)}.mapper.spec.ts`),
|
||||||
|
generatedCount,
|
||||||
|
'specs'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Generate Use Cases and Repositories from Paths/Tags
|
||||||
// Generate per tag
|
// Generate per tag
|
||||||
Object.keys(tagsMap).forEach((tag) => {
|
Object.keys(tagsMap).forEach((tag) => {
|
||||||
const returnImports: { classname: string; classFilename: string; classVarName: string }[] = [];
|
const tagFilename = toCamelCase(tag);
|
||||||
const paramImports: { classname: string; classFilename: string; classVarName: string }[] = [];
|
const returnImports: {
|
||||||
|
classname: string;
|
||||||
|
classFilename: string;
|
||||||
|
classVarName: string;
|
||||||
|
tagFilename: string;
|
||||||
|
}[] = [];
|
||||||
|
const paramImports: {
|
||||||
|
classname: string;
|
||||||
|
classFilename: string;
|
||||||
|
classVarName: string;
|
||||||
|
tagFilename: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
Object.keys(schemas).forEach((s) => {
|
Object.keys(schemas).forEach((s) => {
|
||||||
const usedAsReturn = tagsMap[tag].some(
|
const usedAsReturn = tagsMap[tag].some(
|
||||||
@@ -347,7 +452,12 @@ export function generateCleanArchitecture(
|
|||||||
op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`)
|
op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`)
|
||||||
);
|
);
|
||||||
|
|
||||||
const entry = { classname: s, classFilename: toCamelCase(s), classVarName: toCamelCase(s) };
|
const entry = {
|
||||||
|
classname: s,
|
||||||
|
classFilename: toCamelCase(s),
|
||||||
|
classVarName: toCamelCase(s),
|
||||||
|
tagFilename: schemaTagMap[s] || 'shared'
|
||||||
|
};
|
||||||
|
|
||||||
if (usedAsReturn) {
|
if (usedAsReturn) {
|
||||||
returnImports.push(entry);
|
returnImports.push(entry);
|
||||||
@@ -362,18 +472,14 @@ export function generateCleanArchitecture(
|
|||||||
apis: [
|
apis: [
|
||||||
{
|
{
|
||||||
operations: {
|
operations: {
|
||||||
classname: tag,
|
classname: toPascalCase(tag),
|
||||||
classFilename: toCamelCase(tag),
|
classFilename: tagFilename,
|
||||||
classVarName: toCamelCase(tag),
|
classVarName: tagFilename,
|
||||||
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
||||||
operation: tagsMap[tag],
|
operation: tagsMap[tag],
|
||||||
// All entity imports (return + param) — for contracts and use-cases
|
|
||||||
imports: [...returnImports, ...paramImports],
|
imports: [...returnImports, ...paramImports],
|
||||||
// Return-type-only imports — for repo impl (Dto + Entity + Mapper)
|
|
||||||
returnImports,
|
returnImports,
|
||||||
// Param-only imports — for repo impl (Entity only, no Dto/Mapper)
|
|
||||||
paramImports,
|
paramImports,
|
||||||
// Environment API key for the repository base URL (e.g. "aprovalmApi")
|
|
||||||
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
|
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,7 +491,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.use-cases.contract.mustache',
|
'api.use-cases.contract.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.contract.ts`),
|
path.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.contract.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'useCases'
|
'useCases'
|
||||||
);
|
);
|
||||||
@@ -394,7 +500,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.use-cases.impl.mustache',
|
'api.use-cases.impl.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.ts`),
|
path.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.impl.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'useCases'
|
'useCases'
|
||||||
);
|
);
|
||||||
@@ -403,7 +509,12 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.repository.contract.mustache',
|
'api.repository.contract.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/repositories', `${toCamelCase(tag)}.repository.contract.ts`),
|
path.join(
|
||||||
|
outputDir,
|
||||||
|
'domain/repositories',
|
||||||
|
tagFilename,
|
||||||
|
`${tagFilename}.repository.contract.ts`
|
||||||
|
),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'repositories'
|
'repositories'
|
||||||
);
|
);
|
||||||
@@ -412,7 +523,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.repository.impl.mustache',
|
'api.repository.impl.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.ts`),
|
path.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'repositories'
|
'repositories'
|
||||||
);
|
);
|
||||||
@@ -421,7 +532,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'use-cases.provider.mustache',
|
'use-cases.provider.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.ts`),
|
path.join(outputDir, 'di/use-cases', tagFilename, `${tagFilename}.use-cases.provider.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'providers'
|
'providers'
|
||||||
);
|
);
|
||||||
@@ -430,17 +541,22 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'repository.provider.mustache',
|
'repository.provider.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.ts`),
|
path.join(outputDir, 'di/repositories', tagFilename, `${tagFilename}.repository.provider.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'providers'
|
'providers'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mocks — repository impl, use-cases impl, repository provider, use-cases provider
|
// Mocks
|
||||||
renderTemplate(
|
renderTemplate(
|
||||||
templatesDir,
|
templatesDir,
|
||||||
'api.repository.impl.mock.mustache',
|
'api.repository.impl.mock.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.mock.ts`),
|
path.join(
|
||||||
|
outputDir,
|
||||||
|
'data/repositories',
|
||||||
|
tagFilename,
|
||||||
|
`${tagFilename}.repository.impl.mock.ts`
|
||||||
|
),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'mocks'
|
'mocks'
|
||||||
);
|
);
|
||||||
@@ -449,7 +565,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.use-cases.mock.mustache',
|
'api.use-cases.mock.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.mock.ts`),
|
path.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.mock.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'mocks'
|
'mocks'
|
||||||
);
|
);
|
||||||
@@ -458,7 +574,12 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'repository.provider.mock.mustache',
|
'repository.provider.mock.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.mock.ts`),
|
path.join(
|
||||||
|
outputDir,
|
||||||
|
'di/repositories',
|
||||||
|
tagFilename,
|
||||||
|
`${tagFilename}.repository.provider.mock.ts`
|
||||||
|
),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'mocks'
|
'mocks'
|
||||||
);
|
);
|
||||||
@@ -467,7 +588,12 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'use-cases.provider.mock.mustache',
|
'use-cases.provider.mock.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.mock.ts`),
|
path.join(
|
||||||
|
outputDir,
|
||||||
|
'di/use-cases',
|
||||||
|
tagFilename,
|
||||||
|
`${tagFilename}.use-cases.provider.mock.ts`
|
||||||
|
),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'mocks'
|
'mocks'
|
||||||
);
|
);
|
||||||
@@ -477,7 +603,12 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.repository.impl.spec.mustache',
|
'api.repository.impl.spec.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.spec.ts`),
|
path.join(
|
||||||
|
outputDir,
|
||||||
|
'data/repositories',
|
||||||
|
tagFilename,
|
||||||
|
`${tagFilename}.repository.impl.spec.ts`
|
||||||
|
),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'specs'
|
'specs'
|
||||||
);
|
);
|
||||||
@@ -487,7 +618,12 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.use-cases.impl.spec.mustache',
|
'api.use-cases.impl.spec.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.spec.ts`),
|
path.join(
|
||||||
|
outputDir,
|
||||||
|
'domain/use-cases',
|
||||||
|
tagFilename,
|
||||||
|
`${tagFilename}.use-cases.impl.spec.ts`
|
||||||
|
),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'specs'
|
'specs'
|
||||||
);
|
);
|
||||||
@@ -512,6 +648,7 @@ function renderTemplate(
|
|||||||
if (fs.existsSync(templatePath)) {
|
if (fs.existsSync(templatePath)) {
|
||||||
const template = fs.readFileSync(templatePath, 'utf8');
|
const template = fs.readFileSync(templatePath, 'utf8');
|
||||||
const output = mustache.render(template, viewData);
|
const output = mustache.render(template, viewData);
|
||||||
|
fs.ensureDirSync(path.dirname(destPath));
|
||||||
fs.writeFileSync(destPath, output);
|
fs.writeFileSync(destPath, output);
|
||||||
counter[key]++;
|
counter[key]++;
|
||||||
logDetail(
|
logDetail(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { execSync } from 'child_process';
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { logStep, logSuccess, logError, logDetail } from '../utils/logger';
|
import { logStep, logSuccess, logError, logDetail } from '../utils/logger';
|
||||||
|
import { toPascalCase } from '../utils/name-formatter';
|
||||||
|
|
||||||
/** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
|
/** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
|
||||||
export function generateCode(swaggerFile: string, templatesDir: string): string {
|
export function generateCode(swaggerFile: string, templatesDir: string): string {
|
||||||
@@ -35,8 +36,12 @@ export function generateCode(swaggerFile: string, templatesDir: string): string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Copies the generated DTOs from the temporary directory to the output directory. */
|
/** Copies the generated DTOs from the temporary directory to the output directory, organised by tag subfolder. */
|
||||||
export function organizeFiles(tempDir: string, outputDir: string): void {
|
export function organizeFiles(
|
||||||
|
tempDir: string,
|
||||||
|
outputDir: string,
|
||||||
|
schemaTagMap: Record<string, string> = {}
|
||||||
|
): void {
|
||||||
logStep('Organising generated DTO files...');
|
logStep('Organising generated DTO files...');
|
||||||
|
|
||||||
const sourceDir = path.join(tempDir, 'model');
|
const sourceDir = path.join(tempDir, 'model');
|
||||||
@@ -49,8 +54,14 @@ export function organizeFiles(tempDir: string, outputDir: string): void {
|
|||||||
const files = fs.readdirSync(sourceDir).filter((file) => file.endsWith('.dto.ts'));
|
const files = fs.readdirSync(sourceDir).filter((file) => file.endsWith('.dto.ts'));
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
// file is like "userResponse.dto.ts" → derive PascalCase schema name to look up tag
|
||||||
|
const camelName = file.replace('.dto.ts', '');
|
||||||
|
const pascalName = toPascalCase(camelName);
|
||||||
|
const tagFolder = schemaTagMap[pascalName] || 'shared';
|
||||||
|
|
||||||
const sourcePath = path.join(sourceDir, file);
|
const sourcePath = path.join(sourceDir, file);
|
||||||
const destPath = path.join(destDir, file);
|
const destPath = path.join(destDir, tagFolder, file);
|
||||||
|
fs.ensureDirSync(path.dirname(destPath));
|
||||||
fs.copySync(sourcePath, destPath);
|
fs.copySync(sourcePath, destPath);
|
||||||
filesMoved++;
|
filesMoved++;
|
||||||
logDetail('dto', `${file} → ${path.relative(process.cwd(), destPath)}`);
|
logDetail('dto', `${file} → ${path.relative(process.cwd(), destPath)}`);
|
||||||
@@ -65,58 +76,69 @@ export function addDtoImports(outputDir: string): void {
|
|||||||
logStep('Post-processing generated DTOs...');
|
logStep('Post-processing generated DTOs...');
|
||||||
|
|
||||||
const dtosDir = path.join(outputDir, 'data/dtos');
|
const dtosDir = path.join(outputDir, 'data/dtos');
|
||||||
|
|
||||||
if (!fs.existsSync(dtosDir)) return;
|
if (!fs.existsSync(dtosDir)) return;
|
||||||
|
|
||||||
const files = fs.readdirSync(dtosDir).filter((f) => f.endsWith('.dto.ts'));
|
// Collect all .dto.ts files from all subfolders (1 level deep)
|
||||||
|
const allFiles: { subfolder: string; file: string; fullPath: string }[] = [];
|
||||||
|
|
||||||
// Build a map of DTO classname → file base name (without .ts)
|
const entries = fs.readdirSync(dtosDir);
|
||||||
const dtoMap: Record<string, string> = {};
|
entries.forEach((entry) => {
|
||||||
files.forEach((file) => {
|
const entryPath = path.join(dtosDir, entry);
|
||||||
const content = fs.readFileSync(path.join(dtosDir, file), 'utf8');
|
if (fs.statSync(entryPath).isDirectory()) {
|
||||||
const match = content.match(/export interface (\w+)/);
|
fs.readdirSync(entryPath)
|
||||||
if (match) {
|
.filter((f) => f.endsWith('.dto.ts'))
|
||||||
dtoMap[match[1]] = file.replace('.ts', '');
|
.forEach((file) =>
|
||||||
|
allFiles.push({ subfolder: entry, file, fullPath: path.join(entryPath, file) })
|
||||||
|
);
|
||||||
|
} else if (entry.endsWith('.dto.ts')) {
|
||||||
|
allFiles.push({ subfolder: '', file: entry, fullPath: entryPath });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build map: ClassName → { subfolder, fileBase }
|
||||||
|
const dtoMap: Record<string, { subfolder: string; fileBase: string }> = {};
|
||||||
|
allFiles.forEach(({ subfolder, file, fullPath }) => {
|
||||||
|
const content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
const match = content.match(/export interface (\w+)/);
|
||||||
|
if (match) dtoMap[match[1]] = { subfolder, fileBase: file.replace('.ts', '') };
|
||||||
|
});
|
||||||
|
|
||||||
let filesProcessed = 0;
|
let filesProcessed = 0;
|
||||||
|
|
||||||
files.forEach((file) => {
|
allFiles.forEach(({ subfolder, file, fullPath }) => {
|
||||||
const filePath = path.join(dtosDir, file);
|
const originalContent = fs.readFileSync(fullPath, 'utf8');
|
||||||
const originalContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
let content = originalContent;
|
let content = originalContent;
|
||||||
|
|
||||||
const selfMatch = content.match(/export interface (\w+)/);
|
const selfMatch = content.match(/export interface (\w+)/);
|
||||||
const selfName = selfMatch ? selfMatch[1] : '';
|
const selfName = selfMatch ? selfMatch[1] : '';
|
||||||
|
|
||||||
// Normalize Array<T> → T[] (openapi-generator-cli always outputs Array<T>)
|
|
||||||
content = content.replace(/Array<(\w+)>/g, '$1[]');
|
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<string>();
|
const references = new Set<string>();
|
||||||
const typeRegex = /\b(\w+Dto)\b/g;
|
const typeRegex = /\b(\w+Dto)\b/g;
|
||||||
let match;
|
let match;
|
||||||
while ((match = typeRegex.exec(content)) !== null) {
|
while ((match = typeRegex.exec(content)) !== null) {
|
||||||
if (match[1] !== selfName) {
|
if (match[1] !== selfName) references.add(match[1]);
|
||||||
references.add(match[1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build import lines for each referenced type that exists in the dtoMap
|
|
||||||
const imports: string[] = [];
|
const imports: string[] = [];
|
||||||
references.forEach((ref) => {
|
references.forEach((ref) => {
|
||||||
if (dtoMap[ref]) {
|
if (dtoMap[ref]) {
|
||||||
imports.push(`import { ${ref} } from './${dtoMap[ref]}';`);
|
const { subfolder: refSubfolder, fileBase: refFileBase } = dtoMap[ref];
|
||||||
|
const fromDir = subfolder ? path.join(dtosDir, subfolder) : dtosDir;
|
||||||
|
const toFile = refSubfolder
|
||||||
|
? path.join(dtosDir, refSubfolder, refFileBase)
|
||||||
|
: path.join(dtosDir, refFileBase);
|
||||||
|
let relPath = path.relative(fromDir, toFile).replace(/\\/g, '/');
|
||||||
|
if (!relPath.startsWith('.')) relPath = './' + relPath;
|
||||||
|
imports.push(`import { ${ref} } from '${relPath}';`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (imports.length > 0) {
|
if (imports.length > 0) content = imports.join('\n') + '\n' + content;
|
||||||
content = imports.join('\n') + '\n' + content;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content !== originalContent) {
|
if (content !== originalContent) {
|
||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(fullPath, content);
|
||||||
filesProcessed++;
|
filesProcessed++;
|
||||||
logDetail('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
|
logDetail('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { logStep, logSuccess } from '../utils/logger';
|
import { logStep, logSuccess } from '../utils/logger';
|
||||||
|
import { getExampleMismatches } from '../utils/example-validator';
|
||||||
import type { SwaggerAnalysis, GenerationReport, LintResult } from '../types';
|
import type { SwaggerAnalysis, GenerationReport, LintResult } from '../types';
|
||||||
|
|
||||||
/** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
|
/** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
|
||||||
@@ -41,6 +42,8 @@ export function generateReport(
|
|||||||
return { name: t.name, description: t.description || '', endpoints: endpointCount };
|
return { name: t.name, description: t.description || '', endpoints: endpointCount };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exampleMismatches = getExampleMismatches();
|
||||||
|
|
||||||
const report: GenerationReport = {
|
const report: GenerationReport = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
tags: analysis.tags.length,
|
tags: analysis.tags.length,
|
||||||
@@ -48,6 +51,10 @@ export function generateReport(
|
|||||||
tagDetails,
|
tagDetails,
|
||||||
outputDirectory: outputDir,
|
outputDirectory: outputDir,
|
||||||
linting: lintResult,
|
linting: lintResult,
|
||||||
|
warnings: {
|
||||||
|
exampleMismatches: exampleMismatches.map((m) => ({ ...m })),
|
||||||
|
total: exampleMismatches.length
|
||||||
|
},
|
||||||
structure: {
|
structure: {
|
||||||
dtos: fs.readdirSync(path.join(outputDir, 'data/dtos')).length,
|
dtos: fs.readdirSync(path.join(outputDir, 'data/dtos')).length,
|
||||||
repositories: fs.readdirSync(path.join(outputDir, 'data/repositories')).length,
|
repositories: fs.readdirSync(path.join(outputDir, 'data/repositories')).length,
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ export interface TagDetail {
|
|||||||
endpoints: number;
|
endpoints: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single example/type mismatch detected during mock generation.
|
||||||
|
*/
|
||||||
|
export interface ExampleMismatchEntry {
|
||||||
|
schemaName: string;
|
||||||
|
propertyName: string;
|
||||||
|
declaredType: string;
|
||||||
|
exampleValue: unknown;
|
||||||
|
exampleJsType: string;
|
||||||
|
action: 'coerced' | 'ignored';
|
||||||
|
coercedValue?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Final generation report persisted as `generation-report.json`.
|
* Final generation report persisted as `generation-report.json`.
|
||||||
*/
|
*/
|
||||||
@@ -38,6 +51,10 @@ export interface GenerationReport {
|
|||||||
tagDetails: TagDetail[];
|
tagDetails: TagDetail[];
|
||||||
outputDirectory: string;
|
outputDirectory: string;
|
||||||
linting: LintResult;
|
linting: LintResult;
|
||||||
|
warnings: {
|
||||||
|
exampleMismatches: ExampleMismatchEntry[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
structure: {
|
structure: {
|
||||||
dtos: number;
|
dtos: number;
|
||||||
repositories: number;
|
repositories: number;
|
||||||
|
|||||||
103
src/utils/example-validator.ts
Normal file
103
src/utils/example-validator.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Validates that OpenAPI `example` values match their declared `type`.
|
||||||
|
*
|
||||||
|
* YAML parses unquoted values by native type (e.g. `example: 68131` becomes a JS number
|
||||||
|
* even when the schema declares `type: string`). This module detects such mismatches,
|
||||||
|
* coerces them when possible, and accumulates warnings for the generation report.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ExampleMismatch {
|
||||||
|
schemaName: string;
|
||||||
|
propertyName: string;
|
||||||
|
declaredType: string;
|
||||||
|
exampleValue: unknown;
|
||||||
|
exampleJsType: string;
|
||||||
|
action: 'coerced' | 'ignored';
|
||||||
|
coercedValue?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module-level accumulator — reset between runs via `clearExampleMismatches()`.
|
||||||
|
let mismatches: ExampleMismatch[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an `example` value against a declared OpenAPI `type`.
|
||||||
|
*
|
||||||
|
* @returns `{ valid: true }` when types already match, or
|
||||||
|
* `{ valid: false, coerced: <value> }` when the value was coerced, or
|
||||||
|
* `{ valid: false }` when coercion is not possible (caller should ignore the example).
|
||||||
|
*/
|
||||||
|
export function validateExample(
|
||||||
|
declaredType: string | undefined,
|
||||||
|
example: unknown
|
||||||
|
): { valid: boolean; coerced?: unknown } {
|
||||||
|
if (declaredType === undefined) return { valid: true };
|
||||||
|
|
||||||
|
const jsType = typeof example;
|
||||||
|
|
||||||
|
// ── string declared ──────────────────────────────────────────────────────
|
||||||
|
if (declaredType === 'string') {
|
||||||
|
if (jsType === 'string') return { valid: true };
|
||||||
|
// number or boolean → coerce to string
|
||||||
|
if (jsType === 'number' || jsType === 'boolean') {
|
||||||
|
return { valid: false, coerced: String(example) };
|
||||||
|
}
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── integer / number declared ────────────────────────────────────────────
|
||||||
|
if (declaredType === 'integer' || declaredType === 'number') {
|
||||||
|
if (jsType === 'number') return { valid: true };
|
||||||
|
if (jsType === 'string') {
|
||||||
|
const parsed = Number(example);
|
||||||
|
if (!Number.isNaN(parsed)) return { valid: false, coerced: parsed };
|
||||||
|
return { valid: false }; // unparseable → ignore
|
||||||
|
}
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── boolean declared ─────────────────────────────────────────────────────
|
||||||
|
if (declaredType === 'boolean') {
|
||||||
|
if (jsType === 'boolean') return { valid: true };
|
||||||
|
if (jsType === 'string') {
|
||||||
|
const lower = (example as string).toLowerCase();
|
||||||
|
if (lower === 'true') return { valid: false, coerced: true };
|
||||||
|
if (lower === 'false') return { valid: false, coerced: false };
|
||||||
|
}
|
||||||
|
return { valid: false }; // cannot coerce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other types (object, array, etc.) — no validation
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a mismatch so it can be retrieved later for console warnings and the report.
|
||||||
|
*/
|
||||||
|
export function registerMismatch(
|
||||||
|
schemaName: string,
|
||||||
|
propertyName: string,
|
||||||
|
declaredType: string,
|
||||||
|
exampleValue: unknown,
|
||||||
|
action: 'coerced' | 'ignored',
|
||||||
|
coercedValue?: unknown
|
||||||
|
): void {
|
||||||
|
mismatches.push({
|
||||||
|
schemaName,
|
||||||
|
propertyName,
|
||||||
|
declaredType,
|
||||||
|
exampleValue,
|
||||||
|
exampleJsType: typeof exampleValue,
|
||||||
|
action,
|
||||||
|
coercedValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns all recorded mismatches. */
|
||||||
|
export function getExampleMismatches(): ReadonlyArray<ExampleMismatch> {
|
||||||
|
return mismatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears all recorded mismatches (call before each generation run). */
|
||||||
|
export function clearExampleMismatches(): void {
|
||||||
|
mismatches = [];
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
|
import { validateExample, registerMismatch } from './example-validator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a TypeScript literal string to use as a mock value for a single schema property.
|
* Resolves a TypeScript literal string to use as a mock value for a single schema property.
|
||||||
*
|
*
|
||||||
* Priority chain:
|
* Priority chain:
|
||||||
* $ref mock call → array $ref mock call → enum[0] → example → format fallback → type default
|
* $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 propName Property name (used for format heuristics such as "email").
|
||||||
* @param prop Raw OpenAPI property definition.
|
* @param prop Raw OpenAPI property definition.
|
||||||
* @param context 'dto' generates `mockFooDto()`, 'model' generates `mockFooModel()`.
|
* @param context 'dto' generates `mockFooDto()`, 'model' generates `mockFooModel()`.
|
||||||
|
* @param schemaName Parent schema name (used for mismatch reporting).
|
||||||
*/
|
*/
|
||||||
export function resolveMockValue(
|
export function resolveMockValue(
|
||||||
propName: string,
|
propName: string,
|
||||||
@@ -18,7 +21,8 @@ export function resolveMockValue(
|
|||||||
$ref?: string;
|
$ref?: string;
|
||||||
items?: { $ref?: string; type?: string };
|
items?: { $ref?: string; type?: string };
|
||||||
},
|
},
|
||||||
context: 'dto' | 'model' = 'dto'
|
context: 'dto' | 'model' = 'dto',
|
||||||
|
schemaName = 'unknown'
|
||||||
): string {
|
): string {
|
||||||
const suffix = context === 'dto' ? 'Dto' : 'Model';
|
const suffix = context === 'dto' ? 'Dto' : 'Model';
|
||||||
|
|
||||||
@@ -43,8 +47,22 @@ export function resolveMockValue(
|
|||||||
return typeof first === 'string' ? `'${first}'` : String(first);
|
return typeof first === 'string' ? `'${first}'` : String(first);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Example value from the swagger spec (highest fidelity)
|
// 5. Example value — validated and coerced if needed
|
||||||
if (prop.example !== undefined) return formatLiteral(prop.example);
|
if (prop.example !== undefined) {
|
||||||
|
const result = validateExample(prop.type, prop.example);
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
return formatLiteral(prop.example);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.coerced !== undefined) {
|
||||||
|
registerMismatch(schemaName, propName, prop.type!, prop.example, 'coerced', result.coerced);
|
||||||
|
return formatLiteral(result.coerced);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot coerce — register and fall through to defaults
|
||||||
|
registerMismatch(schemaName, propName, prop.type!, prop.example, 'ignored');
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Format-aware fallbacks (when no example is provided)
|
// 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-time') return `'2024-01-01T00:00:00.000Z'`;
|
||||||
|
|||||||
@@ -1,12 +1,109 @@
|
|||||||
/**
|
/**
|
||||||
* Converts a PascalCase name to camelCase by lowercasing the first character.
|
* Converts a string to PascalCase, handling spaces, hyphens and underscores.
|
||||||
|
* Used to derive class names from schema/tag names.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* toPascalCase('Product Format') // 'ProductFormat'
|
||||||
|
* toPascalCase('user-response') // 'UserResponse'
|
||||||
|
* toPascalCase('UserSchema') // 'UserSchema'
|
||||||
|
*/
|
||||||
|
export function toPascalCase(name: string): string {
|
||||||
|
if (!name) return name;
|
||||||
|
return name
|
||||||
|
.split(/[\s\-_]+/)
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to camelCase, handling spaces, hyphens and underscores.
|
||||||
* Used to derive class filenames and variable names from schema/tag names.
|
* Used to derive class filenames and variable names from schema/tag names.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
|
* toCamelCase('Product Format') // 'productFormat'
|
||||||
* toCamelCase('ProductResponse') // 'productResponse'
|
* toCamelCase('ProductResponse') // 'productResponse'
|
||||||
* toCamelCase('UserSchema') // 'userSchema'
|
* toCamelCase('UserSchema') // 'userSchema'
|
||||||
*/
|
*/
|
||||||
export function toCamelCase(name: string): string {
|
export function toCamelCase(name: string): string {
|
||||||
if (!name) return name;
|
if (!name) return name;
|
||||||
return name.charAt(0).toLowerCase() + name.slice(1);
|
const pascal = toPascalCase(name);
|
||||||
|
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const JS_RESERVED_WORDS = new Set([
|
||||||
|
'abstract',
|
||||||
|
'arguments',
|
||||||
|
'await',
|
||||||
|
'boolean',
|
||||||
|
'break',
|
||||||
|
'byte',
|
||||||
|
'case',
|
||||||
|
'catch',
|
||||||
|
'char',
|
||||||
|
'class',
|
||||||
|
'const',
|
||||||
|
'continue',
|
||||||
|
'debugger',
|
||||||
|
'default',
|
||||||
|
'delete',
|
||||||
|
'do',
|
||||||
|
'double',
|
||||||
|
'else',
|
||||||
|
'enum',
|
||||||
|
'eval',
|
||||||
|
'export',
|
||||||
|
'extends',
|
||||||
|
'false',
|
||||||
|
'final',
|
||||||
|
'finally',
|
||||||
|
'float',
|
||||||
|
'for',
|
||||||
|
'function',
|
||||||
|
'goto',
|
||||||
|
'if',
|
||||||
|
'implements',
|
||||||
|
'import',
|
||||||
|
'in',
|
||||||
|
'instanceof',
|
||||||
|
'int',
|
||||||
|
'interface',
|
||||||
|
'let',
|
||||||
|
'long',
|
||||||
|
'native',
|
||||||
|
'new',
|
||||||
|
'null',
|
||||||
|
'package',
|
||||||
|
'private',
|
||||||
|
'protected',
|
||||||
|
'public',
|
||||||
|
'return',
|
||||||
|
'short',
|
||||||
|
'static',
|
||||||
|
'super',
|
||||||
|
'switch',
|
||||||
|
'synchronized',
|
||||||
|
'this',
|
||||||
|
'throw',
|
||||||
|
'throws',
|
||||||
|
'transient',
|
||||||
|
'true',
|
||||||
|
'try',
|
||||||
|
'typeof',
|
||||||
|
'undefined',
|
||||||
|
'var',
|
||||||
|
'void',
|
||||||
|
'volatile',
|
||||||
|
'while',
|
||||||
|
'with',
|
||||||
|
'yield'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Returns true if the given name is a JS/TS reserved word. */
|
||||||
|
export function isReservedWord(name: string): boolean {
|
||||||
|
return JS_RESERVED_WORDS.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefixes reserved words with `_` to produce a safe identifier. */
|
||||||
|
export function safePropertyName(name: string): string {
|
||||||
|
return isReservedWord(name) ? `_${name}` : name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
{{#imports}}
|
{{#imports}}
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
{{/imports}}
|
{{/imports}}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
import { MockService } from 'ng-mocks';
|
import { MockService } from 'ng-mocks';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
|
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl';
|
||||||
{{#returnImports}}
|
{{#returnImports}}
|
||||||
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
|
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
|
||||||
{{/returnImports}}
|
{{/returnImports}}
|
||||||
|
|
||||||
export const mock{{classname}}RepositoryImpl = () =>
|
export const mock{{classname}}RepositoryImpl = () =>
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import { environment } from '@environment';
|
|||||||
|
|
||||||
import { MRepository } from '@mercadona/core/utils/repository';
|
import { MRepository } from '@mercadona/core/utils/repository';
|
||||||
|
|
||||||
import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
|
import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
|
||||||
{{#returnImports}}
|
{{#returnImports}}
|
||||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
|
import { {{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto';
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
|
import { {{classVarName}}Mapper } from '@/mappers/{{tagFilename}}/{{classFilename}}.mapper';
|
||||||
{{/returnImports}}
|
{{/returnImports}}
|
||||||
{{#paramImports}}
|
{{#paramImports}}
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
{{/paramImports}}
|
{{/paramImports}}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { {{classname}}RepositoryImpl } from './{{classFilename}}.repository.impl';
|
import { {{classname}}RepositoryImpl } from './{{classFilename}}.repository.impl';
|
||||||
{{#returnImports}}
|
{{#returnImports}}
|
||||||
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
|
import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
|
||||||
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
|
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
|
||||||
{{/returnImports}}
|
{{/returnImports}}
|
||||||
|
|
||||||
describe('{{classname}}RepositoryImpl', () => {
|
describe('{{classname}}RepositoryImpl', () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
{{#imports}}
|
{{#imports}}
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
{{/imports}}
|
{{/imports}}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Observable } from 'rxjs';
|
|||||||
|
|
||||||
import { {{classname}}UseCases } from './{{classFilename}}.use-cases.contract';
|
import { {{classname}}UseCases } from './{{classFilename}}.use-cases.contract';
|
||||||
|
|
||||||
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
|
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
|
||||||
{{#imports}}
|
{{#imports}}
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
{{/imports}}
|
{{/imports}}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { of } from 'rxjs';
|
|||||||
|
|
||||||
import { {{classname}}UseCasesImpl } from './{{classFilename}}.use-cases.impl';
|
import { {{classname}}UseCasesImpl } from './{{classFilename}}.use-cases.impl';
|
||||||
|
|
||||||
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
|
import { {{constantName}}_REPOSITORY, {{classname}}Repository } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
|
||||||
{{#returnImports}}
|
{{#returnImports}}
|
||||||
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
|
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
|
||||||
{{/returnImports}}
|
{{/returnImports}}
|
||||||
|
|
||||||
describe('{{classname}}UseCasesImpl', () => {
|
describe('{{classname}}UseCasesImpl', () => {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
import { MockService } from 'ng-mocks';
|
import { MockService } from 'ng-mocks';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
|
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
|
||||||
{{#returnImports}}
|
{{#returnImports}}
|
||||||
import { mock{{classname}}Model } from '@/entities/models/{{classFilename}}.model.mock';
|
import { mock{{classname}}Model } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model.mock';
|
||||||
{{/returnImports}}
|
{{/returnImports}}
|
||||||
|
|
||||||
export const mock{{classname}}UseCasesImpl = () =>
|
export const mock{{classname}}UseCasesImpl = () =>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{#models}}
|
{{#models}}
|
||||||
{{#model}}
|
{{#model}}
|
||||||
{{#mockImports}}
|
{{#mockImports}}
|
||||||
import { mock{{classname}}Dto } from './{{classFilename}}.dto.mock';
|
import { mock{{classname}}Dto } from '{{{importPath}}}';
|
||||||
{{/mockImports}}
|
{{/mockImports}}
|
||||||
import { {{classname}}Dto } from './{{classFilename}}.dto';
|
import { {{classname}}Dto } from './{{classFilename}}.dto';
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import { MapFromFn } from '@mercadona/common/public';
|
import { MapFromFn } from '@mercadona/common/public';
|
||||||
import { Builder } from '@mercadona/common/utils';
|
import { Builder } from '@mercadona/common/utils';
|
||||||
|
|
||||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
|
import { {{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto';
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{classname}} Mapper
|
* {{classname}} Mapper
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
{{#model}}
|
{{#model}}
|
||||||
import { {{classVarName}}Mapper } from './{{classFilename}}.mapper';
|
import { {{classVarName}}Mapper } from './{{classFilename}}.mapper';
|
||||||
|
|
||||||
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
|
import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
|
|
||||||
describe('{{classVarName}}Mapper', () => {
|
describe('{{classVarName}}Mapper', () => {
|
||||||
{{#vars}}
|
{{#vars}}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{#models}}
|
{{#models}}
|
||||||
{{#model}}
|
{{#model}}
|
||||||
{{#imports}}
|
{{#imports}}
|
||||||
import { {{classname}} } from './{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{tagFilename}}/{{classFilename}}.model';
|
||||||
{{/imports}}
|
{{/imports}}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('{{classname}}', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
{{#vars}}
|
{{#vars}}
|
||||||
|
{{#hasMockValue}}
|
||||||
it('should allow setting {{name}}', () => {
|
it('should allow setting {{name}}', () => {
|
||||||
const model = new {{classname}}();
|
const model = new {{classname}}();
|
||||||
const expected = mock{{classname}}Model();
|
const expected = mock{{classname}}Model();
|
||||||
@@ -19,13 +20,16 @@ describe('{{classname}}', () => {
|
|||||||
expect(model.{{name}}).toBe(expected.{{name}});
|
expect(model.{{name}}).toBe(expected.{{name}});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{{/hasMockValue}}
|
||||||
{{/vars}}
|
{{/vars}}
|
||||||
it('should build a valid model from mock', () => {
|
it('should build a valid model from mock', () => {
|
||||||
const model = mock{{classname}}Model();
|
const model = mock{{classname}}Model();
|
||||||
|
|
||||||
expect(model).toBeInstanceOf({{classname}});
|
expect(model).toBeInstanceOf({{classname}});
|
||||||
{{#vars}}
|
{{#vars}}
|
||||||
|
{{#hasMockValue}}
|
||||||
expect(model.{{name}}).toBeDefined();
|
expect(model.{{name}}).toBeDefined();
|
||||||
|
{{/hasMockValue}}
|
||||||
{{/vars}}
|
{{/vars}}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{{#models}}
|
{{#models}}
|
||||||
{{#model}}
|
{{#model}}
|
||||||
import { {{classname}} } from './{{classFilename}}.model';
|
import { {{classname}} } from './{{classFilename}}.model';
|
||||||
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
|
import { {{classVarName}}Mapper } from '@/mappers/{{tagFilename}}/{{classFilename}}.mapper';
|
||||||
import { mock{{classname}}Dto } from '@/dtos/{{classFilename}}.dto.mock';
|
import { mock{{classname}}Dto } from '@/dtos/{{tagFilename}}/{{classFilename}}.dto.mock';
|
||||||
|
|
||||||
export const mock{{classname}}Model = (overrides: Partial<{{classname}}> = {}): {{classname}} =>
|
export const mock{{classname}}Model = (overrides: Partial<{{classname}}> = {}): {{classname}} =>
|
||||||
Object.assign(new {{classname}}(), {
|
Object.assign(new {{classname}}(), {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{{#operations}}
|
{{#operations}}
|
||||||
import { Provider } from '@angular/core';
|
import { Provider } from '@angular/core';
|
||||||
|
|
||||||
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract';
|
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
|
||||||
import { mock{{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl.mock';
|
import { mock{{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl.mock';
|
||||||
|
|
||||||
export function mock{{classname}}Repository(): Provider[] {
|
export function mock{{classname}}Repository(): Provider[] {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{{#operations}}
|
{{#operations}}
|
||||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||||
|
|
||||||
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}.repository.contract';
|
import { {{constantName}}_REPOSITORY } from '@/domain/repositories/{{classFilename}}/{{classFilename}}.repository.contract';
|
||||||
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}.repository.impl';
|
import { {{classname}}RepositoryImpl } from '@/data/repositories/{{classFilename}}/{{classFilename}}.repository.impl';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{classname}} Repository Provider
|
* {{classname}} Repository Provider
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{{#operations}}
|
{{#operations}}
|
||||||
import { Provider } from '@angular/core';
|
import { Provider } from '@angular/core';
|
||||||
|
|
||||||
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
|
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
|
||||||
import { mock{{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.mock';
|
import { mock{{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.mock';
|
||||||
|
|
||||||
export function mock{{classname}}UseCases(): Provider[] {
|
export function mock{{classname}}UseCases(): Provider[] {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{{#operations}}
|
{{#operations}}
|
||||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||||
|
|
||||||
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
|
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
|
||||||
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
|
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{classname}} Use Cases Provider
|
* {{classname}} Use Cases Provider
|
||||||
|
|||||||
Reference in New Issue
Block a user