Architecture, design principles, and contribution guidelines for MSG (Microservice Generator).
- Project Philosophy & Design Principles
- System Architecture
- Development Environment Setup
- Code Generation Patterns
- Development Workflow
- Testing Strategy
- End-to-End Testing
- Contributing Guidelines
- Extending MSG
MSG eliminates microservices boilerplate by transforming SQL statements into complete Spring Boot applications through metadata-driven generation.
1. Single Responsibility Principle Every class has exactly one public method, focused on a single responsibility.
// ✅ Good: Single responsibility
public class GenerateInsertDAO {
public static TypeSpec createInsertDAO(String businessName, InsertMetadata metadata) {
// Implementation
}
}
// ❌ Bad: Multiple responsibilities
public class DAOGenerator {
public TypeSpec createInsertDAO(...) { }
public TypeSpec createUpdateDAO(...) { }
public TypeSpec createSelectDAO(...) { }
}2. Clean Code Architecture Following SOLID, DRY, and YAGNI principles with:
- Self-documenting class and method names
- Orchestration pattern for complex workflows
- Value objects for data encapsulation
- Immutable data structures (Java records)
3. Metadata-Driven Generation Uses database metadata and JDBC parameter extraction instead of complex SQL parsing for reliability and accuracy.
4. Convention Over Configuration Follows Spring Boot and REST API conventions for predictable, maintainable output.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ SQL File │───▶│ MicroService │───▶│ Generated │
│ (Input) │ │ Generator │ │ Microservice │
│ │ │ (Orchestrator) │ │ (Spring Boot) │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
MicroServiceGenerator (CLI Entry Point)
├── SqlFileResolver (File Location & Reading)
├── SqlStatementDetector (Statement Type Detection)
└── Operation-Specific GeneratorsKey Classes:
MicroServiceGenerator.java- Main CLI orchestratorSqlFileResolver.java- Intelligent file resolution with fallbackSqlStatementDetector.java- SQL type detection via regex
Each CRUD operation has its own specialized generator:
SelectMicroserviceGenerator ┌─→ GenerateDTO
InsertMicroserviceGenerator ────┼─→ GenerateController
UpdateMicroserviceGenerator │ GenerateDAO
DeleteMicroserviceGenerator └─→ GenerateSpringBootApp
Files:
src/main/java/com/jfeatures/msg/codegen/generator/SelectMicroserviceGenerator.javasrc/main/java/com/jfeatures/msg/codegen/generator/InsertMicroserviceGenerator.javasrc/main/java/com/jfeatures/msg/codegen/generator/UpdateMicroserviceGenerator.javasrc/main/java/com/jfeatures/msg/codegen/generator/DeleteMicroserviceGenerator.java
Database Metadata Strategy:
- SELECT:
SqlMetadata- Executes query to extract ResultSet metadata - INSERT:
InsertMetadataExtractor- Uses DatabaseMetaData for column info - UPDATE:
UpdateMetadataExtractor- Analyzes SET/WHERE clauses via database metadata - DELETE:
ParameterMetadataExtractor- Uses JDBC parameter metadata
Key Files:
src/main/java/com/jfeatures/msg/codegen/dbmetadata/SqlMetadata.javasrc/main/java/com/jfeatures/msg/codegen/dbmetadata/InsertMetadataExtractor.javasrc/main/java/com/jfeatures/msg/codegen/dbmetadata/UpdateMetadataExtractor.javasrc/main/java/com/jfeatures/msg/codegen/dbmetadata/ParameterMetadataExtractor.java
Metadata Records:
// Immutable data carriers
public record InsertMetadata(String tableName, List<ColumnMetadata> insertColumns, String originalSql) {}
public record UpdateMetadata(String tableName, List<ColumnMetadata> setColumns, List<ColumnMetadata> whereColumns, String originalSql) {}
public record DeleteMetadata(String tableName, List<DBColumn> parameters, String originalSql) {}DTO Generators:
GenerateDTO.java(SELECT response DTOs)GenerateInsertDTO.java(INSERT request DTOs)GenerateUpdateDTO.java(UPDATE request DTOs)GenerateDeleteDTO.java(DELETE request DTOs)
Controller Generators:
GenerateController.java(GET endpoints)GenerateInsertController.java(POST endpoints)GenerateUpdateController.java(PUT endpoints)GenerateDeleteController.java(DELETE endpoints)
DAO Generators:
GenerateDAO.java(SELECT data access)GenerateInsertDAO.java(INSERT data access)GenerateUpdateDAO.java(UPDATE data access)GenerateDeleteDAO.java(DELETE data access)
Configuration Generators:
GenerateSpringBootApp.java(Application main class)GenerateDatabaseConfig.java(Database configuration)
Project Structure Management:
MicroserviceProjectWriter.java- Orchestrates complete project writingProjectDirectoryBuilder.java- Creates Maven-standard directory structureMicroserviceDirectoryCleaner.java- Safely cleans target directories
com.jfeatures.msg.codegen/
├── MicroServiceGenerator.java # Main CLI entry point
├── Generate*.java # Code generators (DAO, DTO, Controller)
├── constants/ # Project constants and enums
│ ├── ProjectConstants.java
│ └── SQLServerDataTypeEnum.java
├── database/ # Database connection handling
│ ├── DataSourceConfig.java
│ └── DatabaseConnectionFactory.java
├── dbmetadata/ # Metadata extraction and records
│ ├── ColumnMetadata.java
│ ├── InsertMetadata.java
│ ├── UpdateMetadata.java
│ ├── DeleteMetadata.java
│ ├── SqlMetadata.java
│ ├── InsertMetadataExtractor.java
│ ├── UpdateMetadataExtractor.java
│ └── ParameterMetadataExtractor.java
├── domain/ # Domain objects
│ └── GeneratedMicroservice.java
├── filesystem/ # File system operations
│ ├── MicroserviceProjectWriter.java
│ ├── ProjectDirectoryBuilder.java
│ └── MicroserviceDirectoryCleaner.java
├── generator/ # High-level generators
│ ├── SelectMicroserviceGenerator.java
│ ├── InsertMicroserviceGenerator.java
│ ├── UpdateMicroserviceGenerator.java
│ └── DeleteMicroserviceGenerator.java
├── jdbc/ # JDBC utilities
│ └── JdbcTemplateFactory.java
├── mapping/ # Result set mapping
│ └── ResultSetToColumnMetadataMapper.java
├── sql/ # SQL processing
│ ├── SqlFileResolver.java
│ ├── SqlStatementDetector.java
│ └── ReadFileFromResources.java
└── util/ # Utility classes
└── CaseUtils.java
- Java 21: Modern Java features including text blocks and records
- Spring Boot 3.x: Latest Spring Boot framework
- JavaPoet: Type-safe code generation library
- Lombok: Reduces boilerplate code
- Jakarta Validation: Input validation annotations
- OpenAPI/Swagger: API documentation generation
- Picocli: Command-line interface framework
- Maven: Build and dependency management
- JUnit 5: Testing framework
- SQL Server JDBC: Database connectivity
- Java 21 or later
- Maven 3.8+
- IDE with Lombok plugin (IntelliJ IDEA, Eclipse, VS Code)
- SQL Server database for testing
# 1. Clone repository
git clone <repository-url>
cd MSG
# 2. Install dependencies
mvn clean install
# 3. Run tests to verify setup
mvn test
# 4. Compile project
mvn clean compile
# 5. Setup database (for testing)
docker-compose up -d --buildIntelliJ IDEA:
- Install Lombok plugin
- Enable annotation processing
- Set Java 21 as project SDK
- Configure Maven auto-import
VS Code:
- Install Extension Pack for Java
- Install Lombok Annotations Support
- Configure Java 21 runtime
Eclipse:
- Install Lombok (download lombok.jar, run installer)
- Set Java 21 as project JRE
- Enable Maven nature
Field Generation with Annotations:
FieldSpec fieldSpec = FieldSpec.builder(String.class, "firstName")
.addModifiers(Modifier.PRIVATE)
.addAnnotation(AnnotationSpec.builder(NotNull.class)
.addMember("message", "$S", "firstName is required for customer creation")
.build())
.build();Method Generation with Parameters:
MethodSpec methodSpec = MethodSpec.methodBuilder("insertCustomer")
.addModifiers(Modifier.PUBLIC)
.returns(int.class)
.addParameter(ParameterSpec.builder(CustomerInsertDTO.class, "request")
.build())
.addStatement("Map<String, Object> params = new HashMap<>()")
.addStatement("params.put($S, request.getFirstName())", "firstName")
.addStatement("return namedParameterJdbcTemplate.update(SQL, params)")
.build();Class Generation with Text Block SQL:
TypeSpec daoClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Component.class)
.addField(FieldSpec.builder(String.class, "SQL")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$S", sqlTextBlock)
.build())
.addMethod(insertMethod)
.build();MSG uses SQLServerDataTypeEnum for database-to-Java type mapping:
public enum SQLServerDataTypeEnum {
INTEGER("int", Integer.class),
VARCHAR("nvarchar", String.class),
CHAR("char", String.class),
TIMESTAMP("datetime2", Timestamp.class),
DATE("date", Date.class),
DECIMAL("decimal", BigDecimal.class);
private final String sqlServerType;
private final Class<?> javaType;
}Usage in Code Generation:
// Convert JDBC type to Java class
SQLServerDataTypeEnum dataType = SQLServerDataTypeEnum.fromJdbcType(jdbcType);
Class<?> javaType = dataType.getJavaType();
// Generate field with proper type
FieldSpec field = FieldSpec.builder(javaType, fieldName)
.addModifiers(Modifier.PRIVATE)
.build();Java Classes: PascalCase
CustomerInsertDAO,CustomerUpdateDTO,CustomerController
Java Fields: camelCase
customerId,firstName,lastUpdate
SQL Parameters: camelCase in maps, but preserve original names in SQL
// Java parameter map
params.put("customerId", request.getCustomerId());
params.put("firstName", request.getFirstName());
// SQL preserves original column names
String sql = """
INSERT INTO customer (customer_id, first_name)
VALUES (:customerId, :firstName)
""";Package Structure: lowercase with business domain
com.jfeatures.msg.customer.daocom.jfeatures.msg.customer.controller
To add support for a new SQL statement type (e.g., MERGE, UPSERT):
1. Create Metadata Extractor:
package com.jfeatures.msg.codegen.dbmetadata;
public class MergeMetadataExtractor {
public MergeMetadata extractMetadata(String sql, DatabaseConnection connection) throws Exception {
// 1. Parse MERGE statement structure
// 2. Extract target table, source data, match conditions
// 3. Determine insert/update column mappings
// 4. Return metadata record
}
}2. Create Metadata Value Object:
public record MergeMetadata(
String targetTable,
String sourceTable,
List<ColumnMetadata> matchColumns,
List<ColumnMetadata> insertColumns,
List<ColumnMetadata> updateColumns,
String originalSql
) {}3. Create Code Generators:
// DTO for MERGE request body
public class GenerateMergeDTO {
public static TypeSpec createMergeDTO(String businessName, MergeMetadata metadata) {
// Generate DTO with match criteria and data fields
}
}
// Controller for MERGE endpoint
public class GenerateMergeController {
public static TypeSpec createMergeController(String businessName, MergeMetadata metadata) {
// Generate POST endpoint with MERGE semantics
}
}
// DAO for MERGE operation
public class GenerateMergeDAO {
public static TypeSpec createMergeDAO(String businessName, MergeMetadata metadata) {
// Generate MERGE SQL with text blocks
}
}4. Create Orchestrator:
package com.jfeatures.msg.codegen.generator;
public class MergeMicroserviceGenerator {
public GeneratedMicroservice generateMicroservice(
String businessName,
String destinationPath,
String sqlContent
) throws Exception {
// 1. Extract metadata
MergeMetadata metadata = extractor.extractMetadata(sqlContent, connection);
// 2. Generate components
TypeSpec dto = GenerateMergeDTO.createMergeDTO(businessName, metadata);
TypeSpec controller = GenerateMergeController.createMergeController(businessName, metadata);
TypeSpec dao = GenerateMergeDAO.createMergeDAO(businessName, metadata);
// 3. Return complete microservice
return GeneratedMicroservice.builder()
.dto(dto)
.controller(controller)
.dao(dao)
.build();
}
}5. Update Main Generator:
// In MicroServiceGenerator.java
switch (statementType) {
case "SELECT" -> new SelectMicroserviceGenerator().generateMicroservice(...);
case "INSERT" -> new InsertMicroserviceGenerator().generateMicroservice(...);
case "UPDATE" -> new UpdateMicroserviceGenerator().generateMicroservice(...);
case "DELETE" -> new DeleteMicroserviceGenerator().generateMicroservice(...);
case "MERGE" -> new MergeMicroserviceGenerator().generateMicroservice(...); // Add this
default -> throw new IllegalArgumentException("Unsupported SQL statement type: " + statementType);
}6. Add SQL File Constants:
// In SqlFileResolver.java
private static final String MERGE_SQL_FILE = "sample_merge_parameterized.sql";
// Add to fallback order
private static final List<String> SQL_FILE_FALLBACK_ORDER = Arrays.asList(
UPDATE_SQL_FILE,
INSERT_SQL_FILE,
DELETE_SQL_FILE,
MERGE_SQL_FILE, // Add here
SELECT_SQL_FILE
);For Database Metadata Extraction (INSERT/UPDATE):
DatabaseMetaData metaData = connection.getConnection().getMetaData();
ResultSet columns = metaData.getColumns(null, null, tableName, null);
List<ColumnMetadata> columnList = new ArrayList<>();
while (columns.next()) {
ColumnMetadata column = ColumnMetadata.builder()
.columnName(columns.getString("COLUMN_NAME"))
.columnType(columns.getInt("DATA_TYPE"))
.columnTypeName(columns.getString("TYPE_NAME"))
.precision(columns.getInt("COLUMN_SIZE"))
.scale(columns.getInt("DECIMAL_DIGITS"))
.isNullable(columns.getInt("NULLABLE") == DatabaseMetaData.columnNullable)
.build();
columnList.add(column);
}For JDBC Parameter Extraction (DELETE):
List<DBColumn> parameters = parameterExtractor.extractParameters(sql, connection);
// Returns parameter metadata with names and types- Unit Tests: All generators and utilities
- Integration Tests: Complete generation workflows
- Reality-Based Testing: Verify generated code compiles and runs
# Run all tests
mvn test
# Run specific test class
mvn test -Dtest=GenerateInsertDAOTest
# Run integration tests
mvn integration-test
# Generate coverage report
mvn jacoco:reportUnit Tests (src/test/java):
- Individual generator classes
- Metadata extractors
- Utility classes
- Type mapping
Integration Tests:
- Complete generation workflows
- Database metadata extraction
- File system operations
Reality-Based Testing Pattern:
@Test
void testGenerateInsertDAO_CreatesValidJavaCode() {
// 1. Create test metadata
InsertMetadata metadata = createTestInsertMetadata();
// 2. Generate code
TypeSpec daoClass = GenerateInsertDAO.createInsertDAO("Customer", metadata);
// 3. Verify structure
assertThat(daoClass.name).isEqualTo("CustomerInsertDAO");
assertThat(daoClass.annotations).contains(Component.class);
// 4. Verify generated code compiles (reality test)
JavaFile javaFile = JavaFile.builder("com.test", daoClass).build();
String generatedCode = javaFile.toString();
// 5. Compile and verify
CompilationResult result = compile(generatedCode);
assertThat(result.isSuccess()).isTrue();
}@ExtendWith(MockitoExtension.class)
class InsertMetadataExtractorTest {
@Mock
private DatabaseConnection mockConnection;
@Mock
private DatabaseMetaData mockMetaData;
@Test
void testExtractMetadata_ValidInsertSQL() throws Exception {
// Setup mocks
when(mockConnection.getConnection().getMetaData()).thenReturn(mockMetaData);
when(mockMetaData.getColumns(any(), any(), eq("customer"), any()))
.thenReturn(createMockResultSet());
// Test extraction
InsertMetadataExtractor extractor = new InsertMetadataExtractor();
InsertMetadata metadata = extractor.extractMetadata(sql, mockConnection);
// Verify results
assertThat(metadata.tableName()).isEqualTo("customer");
assertThat(metadata.insertColumns()).hasSize(3);
}
}MSG includes a comprehensive End-to-End testing framework that validates the complete CRUD API generation workflow from SQL files to fully functional Spring Boot microservices. This ensures that all README commands work correctly and generate production-ready code.
The E2E testing framework consists of three main components:
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ E2E Test Classes │───▶│ Code Generation │───▶│ Validation & │
│ - WorkingE2EGen │ │ Orchestration │ │ Verification │
│ - EndToEndCrud │ │ - SQL Files │ │ - Structure Check │
│ - ApiEndpointTester│ │ - CLI Commands │ │ - Compilation Test │
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
1. CompleteCrudGenerationE2ETest (Primary E2E Test)
- Tests all 4 CRUD operations without Docker dependencies
- Validates complete generation workflow
- Runs quickly and reliably in any environment (~5 seconds)
- ✅ STABLE - Primary E2E test suite
2. SqlStatementDetectionAndCrudGenerationE2ETest (SQL Detection Test)
- Ultra-fast validation of SQL statement type detection
- Tests SQL parsing and validation for different structures
- No external dependencies (~0.2 seconds)
- ✅ STABLE - SQL parsing validation
3. FullStackCrudGenerationWithDatabaseE2ETest (Full Integration Test)
- Uses Testcontainers for real database testing
- Tests REST API endpoints with actual HTTP requests
- Requires Docker for execution (~5-10 minutes)
- 🟡 MOSTLY STABLE - 5/6 tests pass, REST API integration may occasionally fail
4. GeneratedMicroserviceValidator (Code Quality Validator)
- Validates Maven project structure
- Checks Java class generation and annotations
- Verifies Spring Boot configuration files
5. ApiEndpointTester (REST API Tester)
- Tests generated REST endpoints with HTTP requests
- Validates request/response handling
- Checks microservice startup and health
# Run all E2E tests (recommended)
mvn test -Pe2e-tests -Dtest=CompleteCrudGenerationE2ETest,FullStackCrudGenerationWithDatabaseE2ETest,SqlStatementDetectionAndCrudGenerationE2ETest
# Run fast E2E tests without Docker dependencies
mvn test -Pe2e-tests -Dtest=CompleteCrudGenerationE2ETest
# Run with Maven profile (includes unstable tests)
mvn test -Pe2e-tests
# Run specific test method
mvn test -Pe2e-tests -Dtest=CompleteCrudGenerationE2ETest#whenSelectSqlProvidedShouldGenerateCompleteSpringBootMicroserviceWithGetEndpoints# Run full database integration test (mostly stable - REST API may occasionally fail)
mvn test -Pe2e-tests -Dtest=FullStackCrudGenerationWithDatabaseE2ETest
# Run using the convenience script (stable tests only)
./run-e2e-tests.sh
# Run with detailed output
mvn test -Pe2e-tests -XThe E2E tests use a dedicated Maven profile in pom.xml:
<profile>
<id>e2e-tests</id>
<properties>
<skip.unit.tests>false</skip.unit.tests>
<skip.integration.tests>false</skip.integration.tests>
<maven.test.includes>**/*E2E*Test.java,**/*EndToEnd*Test.java</maven.test.includes>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*E2E*Test.java</include>
<include>**/*EndToEnd*Test.java</include>
</includes>
<systemPropertyVariables>
<testcontainers.reuse.enable>true</testcontainers.reuse.enable>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>The E2E tests use dedicated SQL files that mirror real-world scenarios:
src/main/resources/
├── customer_select.sql # SELECT with JOIN and parameters
├── customer_insert.sql # INSERT with validation
├── customer_update.sql # UPDATE with WHERE conditions
└── customer_delete.sql # DELETE with safety parametersExample E2E SQL Files:
-- customer_select.sql
SELECT c.customer_id, c.first_name, c.last_name, c.email
FROM customer c
INNER JOIN address a ON c.address_id = a.address_id
WHERE c.active = :active AND c.customer_id = :customerId
-- customer_insert.sql
INSERT INTO customer (first_name, last_name, email, address_id, active, create_date)
VALUES (:firstName, :lastName, :email, :addressId, :active, GETDATE())
-- customer_update.sql
UPDATE customer
SET first_name = :firstName, last_name = :lastName, email = :email, last_update = :lastUpdate
WHERE customer_id = :customerId AND active = :active
-- customer_delete.sql
DELETE FROM customer
WHERE customer_id = :customerId AND active = :activeFor Testcontainers-based tests, a minimal test schema is used:
-- sakila-test-schema.sql
CREATE TABLE country (
country_id INT IDENTITY(1,1) PRIMARY KEY,
country NVARCHAR(50) NOT NULL,
last_update DATETIME2 DEFAULT GETDATE()
);
CREATE TABLE city (
city_id INT IDENTITY(1,1) PRIMARY KEY,
city NVARCHAR(50) NOT NULL,
country_id INT FOREIGN KEY REFERENCES country(country_id),
last_update DATETIME2 DEFAULT GETDATE()
);
CREATE TABLE address (
address_id INT IDENTITY(1,1) PRIMARY KEY,
address NVARCHAR(50) NOT NULL,
city_id INT FOREIGN KEY REFERENCES city(city_id),
last_update DATETIME2 DEFAULT GETDATE()
);
CREATE TABLE customer (
customer_id INT IDENTITY(1,1) PRIMARY KEY,
first_name NVARCHAR(50) NOT NULL,
last_name NVARCHAR(50) NOT NULL,
email NVARCHAR(50),
address_id INT FOREIGN KEY REFERENCES address(address_id),
active CHAR(1) DEFAULT 'Y',
create_date DATETIME2 DEFAULT GETDATE(),
last_update DATETIME2 DEFAULT GETDATE()
);
-- Sample test data
INSERT INTO country (country) VALUES ('United States'), ('Canada'), ('Mexico');
INSERT INTO city (city, country_id) VALUES ('New York', 1), ('Toronto', 2), ('Mexico City', 3);
INSERT INTO address (address, city_id) VALUES ('123 Main St', 1), ('456 Oak Ave', 2), ('789 Pine Rd', 3);
INSERT INTO customer (first_name, last_name, email, address_id, active)
VALUES ('John', 'Doe', 'john.doe@example.com', 1, 'Y'),
('Jane', 'Smith', 'jane.smith@example.com', 2, 'Y'),
('Bob', 'Johnson', 'bob.johnson@example.com', 3, 'N');@Test
@DisplayName("1. Generate and Validate SELECT CRUD API")
void testGenerateSelectCrudApi() throws IOException {
// Create dedicated directory for SELECT generation
Path selectDir = Files.createTempDirectory(baseTestDir, "select-");
String[] args = {
"--name", businessName + "Select",
"--destination", selectDir.toString(),
"--sql-file", "customer_select.sql"
};
MicroServiceGenerator generator = new MicroServiceGenerator();
CommandLine commandLine = new CommandLine(generator);
int exitCode = commandLine.execute(args);
assertThat(exitCode)
.as("SELECT generation should complete successfully")
.isEqualTo(0);
// Validate generated structure
assertThat(selectDir.resolve("pom.xml")).exists();
assertThat(selectDir.resolve("src/main/java")).exists();
assertThat(selectDir.resolve("src/main/resources")).exists();
}@Test
@DisplayName("5. Validate Generated Java Classes Structure")
void testGeneratedJavaClasses() throws IOException {
// Generate and validate Java class structure
Path javaRoot = testDir.resolve("src/main/java");
// Check for expected Java files
assertThat(Files.walk(javaRoot)
.anyMatch(path -> path.getFileName().toString().contains("Application.java")))
.as("Should have Application class")
.isTrue();
assertThat(Files.walk(javaRoot)
.anyMatch(path -> path.getFileName().toString().contains("Controller.java")))
.as("Should have Controller class")
.isTrue();
assertThat(Files.walk(javaRoot)
.anyMatch(path -> path.getFileName().toString().contains("DAO.java")))
.as("Should have DAO class")
.isTrue();
}@Test
@DisplayName("Test All CRUD Endpoints")
void testAllCrudEndpoints() {
// Start generated microservice
Process microserviceProcess = apiTester.whenProjectRootProvidedShouldStartGeneratedMicroserviceInSeparateProcess(projectRoot);
try {
// Test all CRUD operations
apiTester.whenPostRequestSentShouldCreateCustomerThroughRestEndpointSuccessfully();
apiTester.whenGetRequestSentShouldRetrieveCustomerDataThroughRestEndpointSuccessfully();
apiTester.whenPutRequestSentShouldUpdateCustomerDataThroughRestEndpointSuccessfully();
apiTester.whenDeleteRequestSentShouldRemoveCustomerThroughRestEndpointSuccessfully();
} finally {
apiTester.whenProcessRunningShouldStopMicroserviceGracefully(microserviceProcess);
}
}@Test
@DisplayName("7. Test Error Handling and Edge Cases")
void testErrorHandlingAndEdgeCases() {
// Test with invalid business name
String[] invalidNameArgs = {"--name", "", "--destination", baseTestDir.toString()};
int exitCode = new CommandLine(new MicroServiceGenerator()).execute(invalidNameArgs);
assertThat(exitCode)
.as("Invalid business name should fail gracefully")
.isNotEqualTo(0);
// Test with non-existent SQL file
String[] invalidSqlArgs = {
"--name", "TestBusiness",
"--destination", baseTestDir.toString(),
"--sql-file", "non_existent_file.sql"
};
exitCode = new CommandLine(new MicroServiceGenerator()).execute(invalidSqlArgs);
assertThat(exitCode)
.as("Non-existent SQL file should fail gracefully")
.isNotEqualTo(0);
}@BeforeAll
static void setupTestEnvironment() throws IOException {
// Create isolated test directory for each test run
baseTestDir = Files.createTempDirectory("msg-working-e2e");
}
@AfterAll
static void cleanupTestEnvironment() throws IOException {
// Clean up test directories after completion
if (baseTestDir != null && Files.exists(baseTestDir)) {
Files.walk(baseTestDir)
.sorted(java.util.Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
System.err.println("Failed to delete: " + path);
}
});
}
}private void validateGeneratedProject(Path projectDir) {
// 1. Maven structure validation
assertThat(projectDir.resolve("pom.xml")).exists();
assertThat(projectDir.resolve("src/main/java")).exists();
assertThat(projectDir.resolve("src/main/resources")).exists();
// 2. Java class validation
Path javaRoot = projectDir.resolve("src/main/java");
assertThat(Files.walk(javaRoot).anyMatch(p -> p.toString().contains("Controller"))).isTrue();
assertThat(Files.walk(javaRoot).anyMatch(p -> p.toString().contains("DAO"))).isTrue();
assertThat(Files.walk(javaRoot).anyMatch(p -> p.toString().contains("DTO"))).isTrue();
// 3. Configuration validation
assertThat(projectDir.resolve("src/main/resources/application.properties")).exists();
}// Use separate temporary directories for parallel test execution
Path testDir = Files.createTempDirectory(baseTestDir, operationType.toLowerCase() + "-");
// Enable Testcontainers reuse for faster test execution
@Container
static MSSQLServerContainer<?> sqlServer = new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2022-latest")
.withPassword("TestPassword@123")
.withInitScript("e2e/sakila-test-schema.sql")
.withReuse(true); // Reuse containers across test runs1. Docker Connection Issues
# Error: Could not find a valid Docker environment
# Solution: Ensure Docker is installed and running
docker info
# If Docker is unavailable, run non-Docker E2E tests
mvn test -Pe2e-tests -Dtest=CompleteCrudGenerationE2ETest2. Port Conflicts
# Error: Port 8080 is already in use
# Solution: Stop services on port 8080 or configure different port
lsof -ti:8080 | xargs kill -9
# Or use random port in tests
@Container
static MSSQLServerContainer<?> sqlServer = new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2022-latest")
.withExposedPorts() // Use random available port3. Test Timeout Issues
# Increase test timeout for slow environments
mvn test -Pe2e-tests -Dmaven.surefire.timeout=600
# Or configure in pom.xml
<configuration>
<forkedProcessTimeoutInSeconds>600</forkedProcessTimeoutInSeconds>
</configuration>4. Resource Cleanup Issues
// Ensure proper cleanup in test teardown
@AfterEach
void cleanupTestResources() {
// Stop any running microservices
if (microserviceProcess != null && microserviceProcess.isAlive()) {
microserviceProcess.destroyForcibly();
}
// Clean up temporary files
cleanupTempDirectory(testDir);
}name: E2E Tests
on: [push, pull_request]
jobs:
e2e-tests:
runs-on: ubuntu-latest
services:
docker:
image: docker:19.03.12
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Run E2E Tests
run: |
mvn clean compile
mvn test -Pe2e-tests -Dtest=CompleteCrudGenerationE2ETest
- name: Run Full Integration Tests (if Docker available)
run: |
if docker info > /dev/null 2>&1; then
mvn test -Pe2e-tests -Dtest=FullStackCrudGenerationWithDatabaseE2ETest
else
echo "Docker not available, skipping Testcontainers tests"
fi
- name: Upload E2E Test Results
uses: actions/upload-artifact@v3
if: always()
with:
name: e2e-test-results
path: target/surefire-reports/The E2E tests provide comprehensive coverage of the MSG tool functionality:
- ✅ CRUD API Generation: All 4 operations (SELECT, INSERT, UPDATE, DELETE)
- ✅ Project Structure: Maven pom.xml, directory layout, Spring Boot configuration
- ✅ Code Quality: Java class generation, annotations, method signatures
- ✅ Compilation: Generated code compiles without errors
- ✅ Runtime: Generated microservices start and respond to HTTP requests
- ✅ Error Handling: Invalid inputs handled gracefully
- ✅ Edge Cases: Boundary conditions and error scenarios
Typical Test Execution Times:
- Complete E2E Test Suite: All tests: ~5-10 minutes
- Individual Tests:
- CompleteCrudGenerationE2ETest: ~5 seconds ✅ STABLE
- SqlStatementDetectionAndCrudGenerationE2ETest: ~0.2 seconds ✅ STABLE
- FullStackCrudGenerationWithDatabaseE2ETest: ~5-10 minutes 🟡 MOSTLY STABLE (5/6 tests pass)
- Convenience Script:
./run-e2e-tests.shruns all E2E tests
This comprehensive E2E testing framework ensures that MSG generates high-quality, production-ready microservices that work correctly in real-world scenarios.
1. Single Responsibility Principle
- One public method per class
- Clear, focused functionality
- Self-documenting method names
2. Naming Conventions
- Classes: PascalCase (
GenerateInsertDAO) - Methods: camelCase (
createInsertDAO) - Constants: UPPER_SNAKE_CASE (
INSERT_SQL_FILE)
3. Documentation Requirements
/**
* Generates a Spring Boot DAO class for INSERT operations.
*
* Creates a component class with:
* - @Component annotation for Spring dependency injection
* - Named parameter JDBC template for SQL execution
* - Text block SQL for readability
* - Parameter mapping from DTO to SQL parameters
*
* @param businessName The business domain name (e.g., "Customer", "Product")
* @param metadata The INSERT metadata containing table and column information
* @return TypeSpec representing the generated DAO class
* @throws IllegalArgumentException if businessName or metadata is null
*/
public static TypeSpec createInsertDAO(String businessName, InsertMetadata metadata) {
// Implementation
}4. Error Handling
// ✅ Good: Specific exceptions with context
if (metadata == null) {
throw new IllegalArgumentException("InsertMetadata cannot be null for DAO generation");
}
// ❌ Bad: Generic exceptions
if (metadata == null) {
throw new RuntimeException("Error");
}1. Branch Naming
- Features:
feature/add-postgresql-support - Bugs:
fix/parameter-mapping-bug - Docs:
docs/update-developer-guide
2. Commit Messages
# Good commit messages
feat: Add PostgreSQL database type mapping support
fix: Resolve parameter count mismatch in DELETE generation
docs: Update developer guide with testing patterns
refactor: Extract common DTO field generation logic
# Bad commit messages
fix stuff
update code
changes3. PR Requirements
- All tests pass (
mvn test) - Code coverage maintained or improved
- Documentation updated (README, JavaDoc)
- Manual testing completed
- Backward compatibility preserved
- No security vulnerabilities introduced
4. Review Checklist
- Code follows single responsibility principle
- Generated code compiles and runs correctly
- Test coverage for new functionality
- Documentation is clear and comprehensive
- No hardcoded values or magic strings
1. IDE Setup
# .editorconfig
root = true
[*.java]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true2. Code Formatting
# Use Maven formatter plugin
mvn formatter:format
# Or configure IDE auto-formatting3. Testing Guidelines
- Write tests before implementing features (TDD)
- Use descriptive test method names
- Follow AAA pattern (Arrange, Act, Assert)
- Mock external dependencies
- Test both success and failure scenarios
To add PostgreSQL support:
1. Create PostgreSQL Type Mapping:
public enum PostgreSQLDataTypeEnum {
INTEGER("integer", Integer.class),
VARCHAR("varchar", String.class),
TEXT("text", String.class),
TIMESTAMP("timestamp", Timestamp.class),
BOOLEAN("boolean", Boolean.class);
// Implementation similar to SQLServerDataTypeEnum
}2. Create Database-Specific Configuration:
public class GeneratePostgreSQLConfig {
public static TypeSpec createDatabaseConfig() {
return TypeSpec.classBuilder("DatabaseConfig")
.addAnnotation(Configuration.class)
.addMethod(createDataSourceMethod())
.build();
}
private static MethodSpec createDataSourceMethod() {
return MethodSpec.methodBuilder("dataSource")
.addAnnotation(Bean.class)
.returns(DataSource.class)
.addStatement("// PostgreSQL DataSource configuration")
.build();
}
}3. Update Metadata Extractors:
// Modify constructors to accept database type
public InsertMetadataExtractor(DatabaseType databaseType) {
this.dataTypeMapper = switch(databaseType) {
case SQL_SERVER -> new SQLServerDataTypeMapper();
case POSTGRESQL -> new PostgreSQLDataTypeMapper();
default -> throw new IllegalArgumentException("Unsupported database type");
};
}To customize generated code templates:
1. Create Template Interface:
public interface CodeTemplate {
String generateController(String businessName, Object metadata);
String generateDAO(String businessName, Object metadata);
String generateDTO(String businessName, Object metadata);
}2. Implement Custom Template:
public class ReactiveCodeTemplate implements CodeTemplate {
@Override
public String generateController(String businessName, Object metadata) {
// Generate WebFlux reactive controllers
return """
@RestController
@RequestMapping("/api")
public class ${businessName}Controller {
@PostMapping("/${businessName.toLowerCase()}")
public Mono<ResponseEntity<String>> create(@RequestBody ${businessName}DTO dto) {
return service.create(dto)
.map(result -> ResponseEntity.ok("Created successfully"));
}
}
""".replace("${businessName}", businessName);
}
}3. Configure Template Selection:
// Add to MicroServiceGenerator
@Option(names = "--template", description = "Code generation template")
private String template = "default";
// Use appropriate template
CodeTemplate codeTemplate = switch(template) {
case "reactive" -> new ReactiveCodeTemplate();
case "default" -> new DefaultCodeTemplate();
default -> throw new IllegalArgumentException("Unknown template: " + template);
};This developer guide provides comprehensive information for contributors and maintainers. For usage instructions, see the User Guide.