ruch
Version:
Revolutionary React TypeScript CLI with hexagonal architecture & AI-powered development assistance. Create maintainable, scalable applications with domain-driven design and integrated AI tooling.
629 lines (539 loc) • 19.7 kB
text/typescript
import type { DomainAnalysis, ParsedEntity, ParsedMethod, ParsedAdapter, ParsedPorts } from '../utils/domain-file-analyzer';
/**
* Generate dynamic MSW handlers based on domain analysis
*/
export const generateDynamicMswHandlers = (analysis: DomainAnalysis): string => {
const { domainName, entities, adapters, ports, primaryEntity } = analysis;
const capitalizedDomain = domainName.charAt(0).toUpperCase() + domainName.slice(1);
// Get primary adapter or fallback to first adapter
const primaryAdapter = adapters.find(a =>
a.name.toLowerCase().includes('api') ||
a.name.toLowerCase().includes(domainName.toLowerCase())
) || adapters[0];
const baseUrl = primaryAdapter?.baseUrl || `/api/${domainName}`;
const mockDataPath = `../../../domains/${domainName}/mocks/mockData`;
// Generate imports
const imports = generateImports(entities, mockDataPath, capitalizedDomain);
// Generate handlers based on ports (interface) and adapter for baseUrl
const primaryPort = ports.find(p =>
p.name.toLowerCase().includes('port') ||
p.name.toLowerCase().includes(domainName.toLowerCase())
) || ports[0];
const handlers = primaryPort ?
generateHandlersFromPort(primaryPort, baseUrl, capitalizedDomain) :
primaryAdapter ?
generateHandlersFromAdapter(primaryAdapter, baseUrl, capitalizedDomain) :
generateDefaultHandlers(domainName, baseUrl, capitalizedDomain);
return `// ${capitalizedDomain} Domain MSW Handlers
// Auto-generated based on domain analysis
//
// ⚠️ IMPORTANT: This file is generated based on your Port interface methods.
// The endpoints are generic and need to be customized to match your actual API.
//
// TODO: Update the endpoints and logic to match your real API:
// - Modify API_BASE to your actual base URL
// - Update endpoint paths for each method
// - Customize request/response handling
// - Add proper query parameters for search methods
import { http, HttpResponse } from 'msw';
${imports}
const API_BASE = '${baseUrl}';
export const ${domainName}Handlers = [
${handlers}
];
`;
};
/**
* Generate imports based on entities
*/
const generateImports = (entities: ParsedEntity[], mockDataPath: string, capitalizedDomain: string): string => {
if (entities.length === 0) {
return `import { mock${capitalizedDomain}Data } from '${mockDataPath}';`;
}
const entityImports = entities.map(entity => entity.name).join(', ');
return `import { ${entityImports} } from '../../../domains/${entities[0]?.name.toLowerCase()}/entities';
import { mock${capitalizedDomain}Data } from '${mockDataPath}';`;
};
/**
* Generate handlers based on port methods (preferred approach)
*/
const generateHandlersFromPort = (port: ParsedPorts, baseUrl: string, capitalizedDomain: string): string => {
const handlers: string[] = [];
for (const method of port.methods) {
const handler = generateHandlerFromMethod(method, baseUrl, capitalizedDomain);
if (handler) {
handlers.push(handler);
}
}
// If no handlers generated, add default ones
if (handlers.length === 0) {
return generateDefaultHandlers(port.name, baseUrl, capitalizedDomain);
}
return handlers.join(',\n\n');
};
/**
* Generate handlers based on adapter methods
*/
const generateHandlersFromAdapter = (adapter: ParsedAdapter, baseUrl: string, capitalizedDomain: string): string => {
const handlers: string[] = [];
for (const method of adapter.methods) {
const handler = generateHandlerFromMethod(method, baseUrl, capitalizedDomain);
if (handler) {
handlers.push(handler);
}
}
// If no handlers generated, add default ones
if (handlers.length === 0) {
return generateDefaultHandlers(adapter.name, baseUrl, capitalizedDomain);
}
return handlers.join(',\n\n');
};
/**
* Generate a single handler from a method
*/
const generateHandlerFromMethod = (method: ParsedMethod, baseUrl: string, capitalizedDomain: string): string | null => {
const { name, httpMethod, endpoint, parameters, returnType } = method;
if (!httpMethod) return null;
const fullEndpoint = endpoint ? `\${API_BASE}${endpoint}` : `\${API_BASE}`;
const httpMethodLower = httpMethod.toLowerCase();
// Generate handler based on HTTP method
switch (httpMethod) {
case 'GET':
return generateGetHandler(name, fullEndpoint, returnType, parameters, capitalizedDomain);
case 'POST':
return generatePostHandler(name, fullEndpoint, returnType, capitalizedDomain);
case 'PUT':
return generatePutHandler(name, fullEndpoint, returnType, capitalizedDomain);
case 'DELETE':
return generateDeleteHandler(name, fullEndpoint, returnType, capitalizedDomain);
case 'PATCH':
return generatePatchHandler(name, fullEndpoint, returnType, capitalizedDomain);
default:
return null;
}
};
/**
* Generate GET handler
*/
const generateGetHandler = (methodName: string, endpoint: string, returnType: string, parameters: any[], capitalizedDomain: string): string => {
const hasIdParam = parameters.some(p => p.name.toLowerCase().includes('id'));
const isListMethod = methodName.toLowerCase().includes('list') || methodName.toLowerCase().includes('all');
if (hasIdParam && !isListMethod) {
return ` // GET ${endpoint.replace('${API_BASE}', '')} - ${methodName}
// TODO: Update endpoint path to match your API (e.g., '/:id', '/by-id/:id', etc.)
http.get(\`${endpoint}\`, ({ params }) => {
const { id } = params;
const item = mock${capitalizedDomain}Data.getById(id as string);
if (!item) {
return new HttpResponse(null, {
status: 404,
statusText: '${capitalizedDomain} not found'
});
}
return HttpResponse.json(item);
})`;
} else {
return ` // GET ${endpoint.replace('${API_BASE}', '')} - ${methodName}
// TODO: Update endpoint path and add query parameters if needed
http.get(\`${endpoint}\`, () => {
return HttpResponse.json(mock${capitalizedDomain}Data.getAll());
})`;
}
};
/**
* Generate POST handler
*/
const generatePostHandler = (methodName: string, endpoint: string, returnType: string, capitalizedDomain: string): string => {
return ` // POST ${endpoint.replace('${API_BASE}', '')} - ${methodName}
// TODO: Customize endpoint path and request validation
http.post(\`${endpoint}\`, async ({ request }) => {
try {
const newItem = await request.json();
const createdItem = mock${capitalizedDomain}Data.create(newItem);
return HttpResponse.json(createdItem, { status: 201 });
} catch (error) {
return new HttpResponse(null, {
status: 400,
statusText: 'Invalid ${capitalizedDomain.toLowerCase()} data'
});
}
})`;
};
/**
* Generate PUT handler
*/
const generatePutHandler = (methodName: string, endpoint: string, returnType: string, capitalizedDomain: string): string => {
return ` // PUT ${endpoint.replace('${API_BASE}', '')} - ${methodName}
// TODO: Update endpoint path and validation logic
http.put(\`${endpoint}\`, async ({ params, request }) => {
const { id } = params;
try {
const updates = await request.json();
const updatedItem = mock${capitalizedDomain}Data.update(id as string, updates);
if (!updatedItem) {
return new HttpResponse(null, {
status: 404,
statusText: '${capitalizedDomain} not found'
});
}
return HttpResponse.json(updatedItem);
} catch (error) {
return new HttpResponse(null, {
status: 400,
statusText: 'Invalid ${capitalizedDomain.toLowerCase()} data'
});
}
})`;
};
/**
* Generate DELETE handler
*/
const generateDeleteHandler = (methodName: string, endpoint: string, returnType: string, capitalizedDomain: string): string => {
return ` // DELETE ${endpoint.replace('${API_BASE}', '')} - ${methodName}
// TODO: Customize endpoint path and authorization logic
http.delete(\`${endpoint}\`, ({ params }) => {
const { id } = params;
const deleted = mock${capitalizedDomain}Data.delete(id as string);
if (!deleted) {
return new HttpResponse(null, {
status: 404,
statusText: '${capitalizedDomain} not found'
});
}
return new HttpResponse(null, { status: 204 });
})`;
};
/**
* Generate PATCH handler
*/
const generatePatchHandler = (methodName: string, endpoint: string, returnType: string, capitalizedDomain: string): string => {
return ` // PATCH ${endpoint.replace('${API_BASE}', '')} - ${methodName}
http.patch(\`${endpoint}\`, async ({ params, request }) => {
const { id } = params;
try {
const updates = await request.json();
const updatedItem = mock${capitalizedDomain}Data.update(id as string, updates);
if (!updatedItem) {
return new HttpResponse(null, {
status: 404,
statusText: '${capitalizedDomain} not found'
});
}
return HttpResponse.json(updatedItem);
} catch (error) {
return new HttpResponse(null, {
status: 400,
statusText: 'Invalid ${capitalizedDomain.toLowerCase()} data'
});
}
})`;
};
/**
* Generate default handlers when no adapter methods found
*/
const generateDefaultHandlers = (domainName: string, baseUrl: string, capitalizedDomain: string): string => {
return ` // GET ${baseUrl.replace('/api', '')} - List all ${domainName} items
http.get(\`\${API_BASE}\`, () => {
return HttpResponse.json(mock${capitalizedDomain}Data.getAll());
}),
// GET ${baseUrl.replace('/api', '')}/:id - Get single ${domainName} item
http.get(\`\${API_BASE}/:id\`, ({ params }) => {
const { id } = params;
const item = mock${capitalizedDomain}Data.getById(id as string);
if (!item) {
return new HttpResponse(null, {
status: 404,
statusText: '${capitalizedDomain} not found'
});
}
return HttpResponse.json(item);
}),
// POST ${baseUrl.replace('/api', '')} - Create new ${domainName} item
http.post(\`\${API_BASE}\`, async ({ request }) => {
try {
const newItem = await request.json();
const createdItem = mock${capitalizedDomain}Data.create(newItem);
return HttpResponse.json(createdItem, { status: 201 });
} catch (error) {
return new HttpResponse(null, {
status: 400,
statusText: 'Invalid ${domainName} data'
});
}
}),
// PUT ${baseUrl.replace('/api', '')}/:id - Update ${domainName} item
http.put(\`\${API_BASE}/:id\`, async ({ params, request }) => {
const { id } = params;
try {
const updates = await request.json();
const updatedItem = mock${capitalizedDomain}Data.update(id as string, updates);
if (!updatedItem) {
return new HttpResponse(null, {
status: 404,
statusText: '${capitalizedDomain} not found'
});
}
return HttpResponse.json(updatedItem);
} catch (error) {
return new HttpResponse(null, {
status: 400,
statusText: 'Invalid ${domainName} data'
});
}
}),
// DELETE ${baseUrl.replace('/api', '')}/:id - Delete ${domainName} item
http.delete(\`\${API_BASE}/:id\`, ({ params }) => {
const { id } = params;
const deleted = mock${capitalizedDomain}Data.delete(id as string);
if (!deleted) {
return new HttpResponse(null, {
status: 404,
statusText: '${capitalizedDomain} not found'
});
}
return new HttpResponse(null, { status: 204 });
})`;
};
/**
* Generate dynamic mock data based on entities
*/
export const generateDynamicMockData = (analysis: DomainAnalysis): string => {
const { domainName, entities, primaryEntity } = analysis;
const capitalizedDomain = domainName.charAt(0).toUpperCase() + domainName.slice(1);
if (!primaryEntity || entities.length === 0) {
// Fallback to generic template
return generateGenericMockData(domainName, capitalizedDomain);
}
// Generate interface import
const entityImports = entities.map(e => `import type { ${e.name} } from '../entities/${e.name}';`).join('\n');
const interfaceImport = entityImports;
// Generate mock data with real fields
const mockDataSamples = generateMockDataSamples(primaryEntity, 3);
const mockDataMethods = generateMockDataMethods(primaryEntity, capitalizedDomain);
return `// ${capitalizedDomain} Domain Mock Data
// Auto-generated based on entity analysis
//
// ⚠️ IMPORTANT: This file is generated based on your Entity definitions.
// The mock data structure follows your entity fields but uses generic sample values.
//
// TODO: Customize the mock data to match your business needs:
// - Update sample data with realistic values for your domain
// - Add domain-specific business logic and validation
// - Customize error scenarios and edge cases
// - Add more sophisticated data relationships
${interfaceImport}
/**
* Mock ${domainName} data store
*/
class Mock${capitalizedDomain}Data {
private data: ${primaryEntity.name}[] = [
${mockDataSamples}
];
${mockDataMethods}
}
// Export singleton instance
export const mock${capitalizedDomain}Data = new Mock${capitalizedDomain}Data();
`;
};
/**
* Generate mock data samples based on entity fields
*/
const generateMockDataSamples = (entity: ParsedEntity, count: number): string => {
const samples: string[] = [];
for (let i = 1; i <= count; i++) {
const fields = entity.fields.map(field => {
const value = generateMockValue(field.type, field.name, i);
return ` ${field.name}: ${value}`;
}).join(',\n');
samples.push(` {\n${fields}\n }`);
}
return samples.join(',\n');
};
/**
* Generate mock value based on field type
*/
const generateMockValue = (type: string, fieldName: string, index: number): string => {
const lowerFieldName = fieldName.toLowerCase();
const lowerType = type.toLowerCase();
// Handle specific field names
if (lowerFieldName.includes('id')) {
return `'${index}'`;
}
if (lowerFieldName.includes('email')) {
return `'user${index}@example.com'`;
}
if (lowerFieldName.includes('name') || lowerFieldName.includes('title')) {
return `'Sample ${fieldName} ${index}'`;
}
if (lowerFieldName.includes('date') || lowerFieldName.includes('time') || lowerType.includes('date')) {
const date = new Date();
date.setDate(date.getDate() + index);
return `new Date('${date.toISOString()}')`;
}
if (lowerFieldName.includes('url') || lowerFieldName.includes('link')) {
return `'https://example.com/${fieldName.toLowerCase()}${index}'`;
}
if (lowerFieldName.includes('phone')) {
return `'+1234567890${index}'`;
}
if (lowerFieldName.includes('price') || lowerFieldName.includes('cost') || lowerFieldName.includes('amount')) {
return `${(index * 10).toFixed(2)}`;
}
// Handle union types (e.g., 'admin' | 'customer' | 'vendor')
if (type.includes('|')) {
const options = type.split('|').map(t => t.trim().replace(/['"]/g, ''));
const validOptions = options.filter(opt => opt && opt.length > 0);
if (validOptions.length > 0) {
const selectedOption = validOptions[(index - 1) % validOptions.length];
return `'${selectedOption}'`;
}
}
// Handle types
if (lowerType.includes('string')) {
return `'Sample ${fieldName} ${index}'`;
}
if (lowerType.includes('number')) {
return `${index * 10}`;
}
if (lowerType.includes('boolean')) {
return index % 2 === 0 ? 'true' : 'false';
}
if (lowerType.includes('array')) {
return `[]`;
}
if (lowerType.includes('object') || lowerType.includes('{')) {
return `{}`;
}
// Default to string
return `'Sample ${fieldName} ${index}'`;
};
/**
* Generate mock data methods
*/
const generateMockDataMethods = (entity: ParsedEntity, capitalizedDomain: string): string => {
return ` /**
* Get all ${entity.name.toLowerCase()} items
*/
getAll(): ${entity.name}[] {
return [...this.data];
}
/**
* Get ${entity.name.toLowerCase()} item by ID
*/
getById(id: string): ${entity.name} | undefined {
return this.data.find(item => item.id === id);
}
/**
* Create new ${entity.name.toLowerCase()} item
*/
create(item: Partial<${entity.name}>): ${entity.name} {
const newItem: ${entity.name} = {
id: Date.now().toString(),
...this.getDefaultValues(),
...item,
} as ${entity.name};
this.data.push(newItem);
return newItem;
}
/**
* Update ${entity.name.toLowerCase()} item
*/
update(id: string, updates: Partial<${entity.name}>): ${entity.name} | undefined {
const index = this.data.findIndex(item => item.id === id);
if (index === -1) {
return undefined;
}
this.data[index] = {
...this.data[index],
...updates,
};
return this.data[index];
}
/**
* Delete ${entity.name.toLowerCase()} item
*/
delete(id: string): boolean {
const index = this.data.findIndex(item => item.id === id);
if (index === -1) {
return false;
}
this.data.splice(index, 1);
return true;
}
/**
* Reset data to initial state
*/
reset(): void {
this.data = [
${generateMockDataSamples(entity, 3)}
];
}
/**
* Get default values for new items
*/
private getDefaultValues(): Partial<${entity.name}> {
return {
${entity.fields.filter(f => !f.optional).map(f =>
` ${f.name}: ${generateMockValue(f.type, f.name, 1)}`
).join(',\n')}
};
}`;
};
/**
* Generate generic mock data when no entities found
*/
const generateGenericMockData = (domainName: string, capitalizedDomain: string): string => {
return `// ${capitalizedDomain} Domain Mock Data
// Generic template - customize based on your domain entities
//
// ⚠️ IMPORTANT: No entities were found in this domain.
// This is a generic template that should be customized for your needs.
//
// TODO: Customize this template to match your business needs:
// - Replace the generic interface with your actual entity types
// - Update sample data with realistic values for your domain
// - Add domain-specific business logic and validation
// - Import your actual entity types from ../entities/
interface ${capitalizedDomain} {
id: string;
name: string;
createdAt: string;
updatedAt: string;
// Add your domain-specific fields here
}
/**
* Mock ${domainName} data store
*/
class Mock${capitalizedDomain}Data {
private data: ${capitalizedDomain}[] = [
{
id: '1',
name: 'Sample ${capitalizedDomain} 1',
createdAt: '2025-01-01T00:00:00Z',
updatedAt: '2025-01-01T00:00:00Z',
},
{
id: '2',
name: 'Sample ${capitalizedDomain} 2',
createdAt: '2025-01-02T00:00:00Z',
updatedAt: '2025-01-02T00:00:00Z',
},
{
id: '3',
name: 'Sample ${capitalizedDomain} 3',
createdAt: '2025-01-03T00:00:00Z',
updatedAt: '2025-01-03T00:00:00Z',
},
];
// Standard CRUD methods...
getAll(): ${capitalizedDomain}[] { return [...this.data]; }
getById(id: string): ${capitalizedDomain} | undefined { return this.data.find(item => item.id === id); }
create(item: Partial<${capitalizedDomain}>): ${capitalizedDomain} { /* implementation */ }
update(id: string, updates: Partial<${capitalizedDomain}>): ${capitalizedDomain} | undefined { /* implementation */ }
delete(id: string): boolean { /* implementation */ }
reset(): void { /* implementation */ }
}
// Export singleton instance
export const mock${capitalizedDomain}Data = new Mock${capitalizedDomain}Data();
`;
};