diff --git a/CLAUDE.md b/CLAUDE.md
index 766fa14f6..593f290f3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -396,6 +396,40 @@ When a change fails and cannot be rolled back:
**File**: `core/flamingock-core-api/src/main/java/io/flamingock/api/RecoveryStrategy.java`
+### Compile-Time Template Validation
+
+Templates are validated at compile-time to ensure YAML structure matches the template type:
+
+**SimpleTemplate** (`AbstractSimpleTemplate`):
+- MUST have `apply` field
+- MAY have `rollback` field
+- MUST NOT have `steps` field
+
+**SteppableTemplate** (`AbstractSteppableTemplate`):
+- MUST have `steps` field
+- MUST NOT have `apply` or `rollback` fields at root level
+- Each step MUST have `apply` field
+
+**Configuration:**
+```java
+@EnableFlamingock(
+ configFile = "pipeline.yaml",
+ strictTemplateValidation = true // default
+)
+```
+
+| Flag Value | Behavior |
+|------------|----------|
+| `true` (default) | Compilation fails with detailed error |
+| `false` | Warning logged, compilation continues |
+
+**Validation Location:** `TemplateValidator` in `core/flamingock-core-commons/.../template/`
+
+**Key Files:**
+- `io.flamingock.internal.common.core.template.TemplateValidator` - validation logic
+- `io.flamingock.api.annotations.EnableFlamingock` - strictTemplateValidation flag
+- `io.flamingock.api.template.AbstractChangeTemplate` - template base classes
+
### Dependency Injection in Templates
Template methods (`@Apply`, `@Rollback`) receive dependencies as **method parameters**, not constructor injection:
diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java
index 2429835c1..3c0b54ae0 100644
--- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java
+++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java
@@ -127,4 +127,27 @@
* When false, only a warning is emitted.
*/
boolean strictStageMapping() default true;
+
+ /**
+ * If true, the annotation processor validates that all template-based changes
+ * have YAML structure matching their template type (Simple vs Steppable).
+ *
+ * SimpleTemplate validation:
+ *
+ * - MUST have {@code apply} field
+ * - MAY have {@code rollback} field
+ * - MUST NOT have {@code steps} field
+ *
+ *
+ * SteppableTemplate validation:
+ *
+ * - MUST have {@code steps} field
+ * - MUST NOT have {@code apply} or {@code rollback} fields at root level
+ * - Each step MUST have {@code apply} field
+ *
+ *
+ * When validation fails and this flag is {@code true} (default), a RuntimeException
+ * is thrown at compilation time. When {@code false}, only a warning is emitted.
+ */
+ boolean strictTemplateValidation() default true;
}
diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java
index 708223a70..a148d3a4e 100644
--- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java
+++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java
@@ -44,12 +44,13 @@ public abstract class AbstractChangeTemplate> reflectiveClasses;
+ private final Set> additionalReflectiveClasses;
@SuppressWarnings("unchecked")
public AbstractChangeTemplate(Class>... additionalReflectiveClass) {
- reflectiveClasses = new HashSet<>(Arrays.asList(additionalReflectiveClass));
+ // Store additional classes - reflective classes set is built on-demand in getReflectiveClasses()
+ this.additionalReflectiveClasses = new HashSet<>(Arrays.asList(additionalReflectiveClass));
try {
Class>[] typeArgs = ReflectionUtil.resolveTypeArgumentsAsClasses(this.getClass(), AbstractChangeTemplate.class);
@@ -61,11 +62,6 @@ public AbstractChangeTemplate(Class>... additionalReflectiveClass) {
this.configurationClass = (Class) typeArgs[0];
this.applyPayloadClass = (Class) typeArgs[1];
this.rollbackPayloadClass = (Class) typeArgs[2];
-
- reflectiveClasses.add(configurationClass);
- reflectiveClasses.add(applyPayloadClass);
- reflectiveClasses.add(rollbackPayloadClass);
- reflectiveClasses.add(TemplateStep.class);
} catch (ClassCastException e) {
throw new IllegalStateException("Generic type arguments for a Template must be concrete types (classes, interfaces, or primitive wrappers like String, Integer, etc.): " + e.getMessage(), e);
} catch (Exception e) {
@@ -73,8 +69,30 @@ public AbstractChangeTemplate(Class>... additionalReflectiveClass) {
}
}
+ /**
+ * Returns the collection of classes that need reflection registration for GraalVM native images.
+ *
+ * This method builds the reflective classes set on-demand, including:
+ *
+ * - The configuration class (generic type argument 0)
+ * - The apply payload class (generic type argument 1)
+ * - The rollback payload class (generic type argument 2)
+ * - {@link TemplateStep} class
+ * - Any additional classes passed to the constructor
+ *
+ *
+ * This method is only called by GraalVM's {@code RegistrationFeature} at build-time,
+ * so there is no performance concern from building the set on each call.
+ *
+ * @return collection of classes requiring reflection registration
+ */
@Override
public final Collection> getReflectiveClasses() {
+ Set> reflectiveClasses = new HashSet<>(additionalReflectiveClasses);
+ reflectiveClasses.add(configurationClass);
+ reflectiveClasses.add(applyPayloadClass);
+ reflectiveClasses.add(rollbackPayloadClass);
+ reflectiveClasses.add(TemplateStep.class);
return reflectiveClasses;
}
diff --git a/core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java b/core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java
new file mode 100644
index 000000000..5b38e9564
--- /dev/null
+++ b/core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.api.template;
+
+import io.flamingock.api.annotations.Apply;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AbstractChangeTemplateReflectiveClassesTest {
+
+ // Simple test configuration class
+ public static class TestConfig {
+ public String configValue;
+ }
+
+ // Simple test apply payload class
+ public static class TestApplyPayload {
+ public String applyData;
+ }
+
+ // Simple test rollback payload class
+ public static class TestRollbackPayload {
+ public String rollbackData;
+ }
+
+ // Additional class for reflection
+ public static class AdditionalClass {
+ public String additionalData;
+ }
+
+ // Another additional class for reflection
+ public static class AnotherAdditionalClass {
+ public String moreData;
+ }
+
+ // Test template with custom generic types
+ public static class TestTemplateWithCustomTypes
+ extends AbstractSimpleTemplate {
+
+ public TestTemplateWithCustomTypes() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ // Test template with additional reflective classes
+ public static class TestTemplateWithAdditionalClasses
+ extends AbstractSimpleTemplate {
+
+ public TestTemplateWithAdditionalClasses() {
+ super(AdditionalClass.class, AnotherAdditionalClass.class);
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ // Test template with Void configuration
+ public static class TestTemplateWithVoidConfig
+ extends AbstractSimpleTemplate {
+
+ public TestTemplateWithVoidConfig() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses should return set containing configuration class")
+ void getReflectiveClassesShouldContainConfigurationClass() {
+ TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.contains(TestConfig.class),
+ "Should contain configuration class TestConfig");
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses should return set containing apply payload class")
+ void getReflectiveClassesShouldContainApplyPayloadClass() {
+ TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.contains(TestApplyPayload.class),
+ "Should contain apply payload class TestApplyPayload");
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses should return set containing rollback payload class")
+ void getReflectiveClassesShouldContainRollbackPayloadClass() {
+ TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.contains(TestRollbackPayload.class),
+ "Should contain rollback payload class TestRollbackPayload");
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses should return set containing TemplateStep class")
+ void getReflectiveClassesShouldContainTemplateStepClass() {
+ TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.contains(TemplateStep.class),
+ "Should contain TemplateStep class");
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses should include additional reflective classes passed to constructor")
+ void getReflectiveClassesShouldIncludeAdditionalClasses() {
+ TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.contains(AdditionalClass.class),
+ "Should contain AdditionalClass");
+ assertTrue(reflectiveClasses.contains(AnotherAdditionalClass.class),
+ "Should contain AnotherAdditionalClass");
+ }
+
+ @Test
+ @DisplayName("Multiple calls to getReflectiveClasses should return equivalent sets")
+ void multipleCallsShouldReturnEquivalentSets() {
+ TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
+
+ Collection> firstCall = template.getReflectiveClasses();
+ Collection> secondCall = template.getReflectiveClasses();
+
+ assertEquals(firstCall.size(), secondCall.size(),
+ "Both calls should return sets of the same size");
+ assertTrue(firstCall.containsAll(secondCall),
+ "First call should contain all elements of second call");
+ assertTrue(secondCall.containsAll(firstCall),
+ "Second call should contain all elements of first call");
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses with Void configuration should include Void class")
+ void getReflectiveClassesWithVoidConfigShouldIncludeVoidClass() {
+ TestTemplateWithVoidConfig template = new TestTemplateWithVoidConfig();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.contains(Void.class),
+ "Should contain Void class for configuration");
+ assertTrue(reflectiveClasses.contains(String.class),
+ "Should contain String class for apply/rollback payloads");
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses should return at least 4 classes (config, apply, rollback, TemplateStep)")
+ void getReflectiveClassesShouldReturnAtLeast4Classes() {
+ TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.size() >= 4,
+ "Should return at least 4 classes (config, apply, rollback, TemplateStep)");
+ }
+
+ @Test
+ @DisplayName("getReflectiveClasses with additional classes should return more than 4 classes")
+ void getReflectiveClassesWithAdditionalClassesShouldReturnMoreThan4() {
+ TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses();
+
+ Collection> reflectiveClasses = template.getReflectiveClasses();
+
+ assertTrue(reflectiveClasses.size() >= 6,
+ "Should return at least 6 classes (config, apply, rollback, TemplateStep, + 2 additional)");
+ }
+}
diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateValidator.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateValidator.java
new file mode 100644
index 000000000..667f23064
--- /dev/null
+++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateValidator.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.internal.common.core.template;
+
+import io.flamingock.api.template.AbstractSimpleTemplate;
+import io.flamingock.api.template.AbstractSteppableTemplate;
+import io.flamingock.api.template.ChangeTemplate;
+import io.flamingock.internal.common.core.error.validation.ValidationError;
+import io.flamingock.internal.common.core.error.validation.ValidationResult;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Validates that template-based YAML changes have the correct structure for their template type.
+ *
+ * This validator ensures:
+ *
+ * - SimpleTemplate: Must have {@code apply}, may have {@code rollback}, must NOT have {@code steps}
+ * - SteppableTemplate: Must have {@code steps}, must NOT have {@code apply} or {@code rollback} at root level
+ *
+ *
+ * The validator is used during compile-time by the annotation processor to catch structural
+ * mismatches early. Behavior is controlled by the {@code strictTemplateValidation} flag in
+ * {@code @EnableFlamingock}:
+ *
+ * - {@code true} (default): Compilation fails with detailed error
+ * - {@code false}: Warning logged, compilation continues
+ *
+ */
+public class TemplateValidator {
+
+ /**
+ * Enumeration of template types for validation purposes.
+ */
+ public enum TemplateType {
+ /**
+ * Template extends AbstractSimpleTemplate - uses apply/rollback fields.
+ */
+ SIMPLE,
+ /**
+ * Template extends AbstractSteppableTemplate - uses steps field.
+ */
+ STEPPABLE,
+ /**
+ * Template type could not be determined.
+ */
+ UNKNOWN
+ }
+
+ private static final String ENTITY_TYPE = "template-change";
+
+ /**
+ * Creates a new TemplateValidator and ensures templates are loaded.
+ */
+ public TemplateValidator() {
+ ChangeTemplateManager.loadTemplates();
+ }
+
+ /**
+ * Validates the YAML content structure against the template type.
+ *
+ * @param content the parsed YAML content
+ * @return ValidationResult containing any validation errors found
+ */
+ public ValidationResult validate(ChangeTemplateFileContent content) {
+ ValidationResult result = new ValidationResult("Template structure validation");
+
+ String templateName = content.getTemplate();
+ String changeId = content.getId() != null ? content.getId() : "unknown";
+
+ if (templateName == null || templateName.trim().isEmpty()) {
+ result.add(new ValidationError("Template name is required", changeId, ENTITY_TYPE));
+ return result;
+ }
+
+ Optional>> templateClassOpt = ChangeTemplateManager.getTemplate(templateName);
+
+ if (!templateClassOpt.isPresent()) {
+ result.add(new ValidationError(
+ "Template '" + templateName + "' not found. Ensure the template is registered via SPI.",
+ changeId,
+ ENTITY_TYPE
+ ));
+ return result;
+ }
+
+ Class extends ChangeTemplate, ?, ?>> templateClass = templateClassOpt.get();
+ TemplateType type = getTemplateType(templateClass);
+
+ switch (type) {
+ case SIMPLE:
+ validateSimpleTemplate(content, changeId, result);
+ break;
+ case STEPPABLE:
+ validateSteppableTemplate(content, changeId, result);
+ break;
+ case UNKNOWN:
+ // Unknown types are valid - they may have custom structure
+ break;
+ }
+
+ return result;
+ }
+
+ /**
+ * Determines the template type based on the class hierarchy.
+ *
+ * @param templateClass the template class to check
+ * @return the TemplateType (SIMPLE, STEPPABLE, or UNKNOWN)
+ */
+ public TemplateType getTemplateType(Class extends ChangeTemplate, ?, ?>> templateClass) {
+ if (AbstractSimpleTemplate.class.isAssignableFrom(templateClass)) {
+ return TemplateType.SIMPLE;
+ } else if (AbstractSteppableTemplate.class.isAssignableFrom(templateClass)) {
+ return TemplateType.STEPPABLE;
+ }
+ return TemplateType.UNKNOWN;
+ }
+
+ /**
+ * Validates a SimpleTemplate structure:
+ * - Must have apply
+ * - May have rollback
+ * - Must NOT have steps
+ */
+ private void validateSimpleTemplate(ChangeTemplateFileContent content, String changeId, ValidationResult result) {
+ // Validate: apply is required
+ if (content.getApply() == null) {
+ result.add(new ValidationError(
+ "SimpleTemplate requires 'apply' field",
+ changeId,
+ ENTITY_TYPE
+ ));
+ }
+
+ // Validate: steps must NOT be present
+ if (content.getSteps() != null) {
+ result.add(new ValidationError(
+ "SimpleTemplate must not have 'steps' field. Use 'apply' and 'rollback' instead.",
+ changeId,
+ ENTITY_TYPE
+ ));
+ }
+ }
+
+ /**
+ * Validates a SteppableTemplate structure:
+ * - Must have steps
+ * - Must NOT have apply at root level
+ * - Must NOT have rollback at root level
+ * - Each step must have apply
+ */
+ @SuppressWarnings("unchecked")
+ private void validateSteppableTemplate(ChangeTemplateFileContent content, String changeId, ValidationResult result) {
+ // Validate: apply must NOT be present at root level
+ if (content.getApply() != null) {
+ result.add(new ValidationError(
+ "SteppableTemplate must not have 'apply' at root level. Define 'apply' within each step.",
+ changeId,
+ ENTITY_TYPE
+ ));
+ }
+
+ // Validate: rollback must NOT be present at root level
+ if (content.getRollback() != null) {
+ result.add(new ValidationError(
+ "SteppableTemplate must not have 'rollback' at root level. Define 'rollback' within each step.",
+ changeId,
+ ENTITY_TYPE
+ ));
+ }
+
+ // Validate: steps is required
+ Object steps = content.getSteps();
+ if (steps == null) {
+ result.add(new ValidationError(
+ "SteppableTemplate requires 'steps' field",
+ changeId,
+ ENTITY_TYPE
+ ));
+ return;
+ }
+
+ // Validate each step has apply
+ if (steps instanceof List) {
+ List> stepList = (List>) steps;
+ for (int i = 0; i < stepList.size(); i++) {
+ Object step = stepList.get(i);
+ if (step instanceof Map) {
+ Map stepMap = (Map) step;
+ if (!stepMap.containsKey("apply") || stepMap.get("apply") == null) {
+ result.add(new ValidationError(
+ "Step " + (i + 1) + " is missing required 'apply' field",
+ changeId,
+ ENTITY_TYPE
+ ));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/TemplateValidatorTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/TemplateValidatorTest.java
new file mode 100644
index 000000000..82975e039
--- /dev/null
+++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/TemplateValidatorTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.internal.common.core.template;
+
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.template.AbstractSimpleTemplate;
+import io.flamingock.api.template.AbstractSteppableTemplate;
+import io.flamingock.internal.common.core.error.validation.ValidationResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class TemplateValidatorTest {
+
+ private TemplateValidator validator;
+
+ // Test template extending AbstractSimpleTemplate
+ public static class TestSimpleTemplate extends AbstractSimpleTemplate {
+ public TestSimpleTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ // Test template extending AbstractSteppableTemplate
+ public static class TestSteppableTemplate extends AbstractSteppableTemplate {
+ public TestSteppableTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ @BeforeEach
+ void setUp() {
+ // Register test templates
+ ChangeTemplateManager.addTemplate("TestSimpleTemplate", TestSimpleTemplate.class);
+ ChangeTemplateManager.addTemplate("TestSteppableTemplate", TestSteppableTemplate.class);
+ validator = new TemplateValidator();
+ }
+
+ @Nested
+ @DisplayName("getTemplateType tests")
+ class GetTemplateTypeTests {
+
+ @Test
+ @DisplayName("Should return SIMPLE for AbstractSimpleTemplate subclass")
+ void shouldReturnSimpleForAbstractSimpleTemplateSubclass() {
+ TemplateValidator.TemplateType type = validator.getTemplateType(TestSimpleTemplate.class);
+ assertEquals(TemplateValidator.TemplateType.SIMPLE, type);
+ }
+
+ @Test
+ @DisplayName("Should return STEPPABLE for AbstractSteppableTemplate subclass")
+ void shouldReturnSteppableForAbstractSteppableTemplateSubclass() {
+ TemplateValidator.TemplateType type = validator.getTemplateType(TestSteppableTemplate.class);
+ assertEquals(TemplateValidator.TemplateType.STEPPABLE, type);
+ }
+ }
+
+ @Nested
+ @DisplayName("SimpleTemplate validation tests")
+ class SimpleTemplateValidationTests {
+
+ @Test
+ @DisplayName("SimpleTemplate with apply only should pass validation")
+ void simpleTemplateWithApplyOnlyPasses() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-1");
+ content.setTemplate("TestSimpleTemplate");
+ content.setApply("CREATE TABLE users");
+
+ ValidationResult result = validator.validate(content);
+
+ assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage());
+ }
+
+ @Test
+ @DisplayName("SimpleTemplate with apply and rollback should pass validation")
+ void simpleTemplateWithApplyAndRollbackPasses() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-2");
+ content.setTemplate("TestSimpleTemplate");
+ content.setApply("CREATE TABLE users");
+ content.setRollback("DROP TABLE users");
+
+ ValidationResult result = validator.validate(content);
+
+ assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage());
+ }
+
+ @Test
+ @DisplayName("SimpleTemplate with steps should fail validation")
+ void simpleTemplateWithStepsFails() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-3");
+ content.setTemplate("TestSimpleTemplate");
+ content.setApply("CREATE TABLE users");
+ content.setSteps(Arrays.asList(createStep("step1", null)));
+
+ ValidationResult result = validator.validate(content);
+
+ assertTrue(result.hasErrors());
+ assertTrue(result.formatMessage().contains("SimpleTemplate must not have 'steps' field"));
+ }
+
+ @Test
+ @DisplayName("SimpleTemplate missing apply should fail validation")
+ void simpleTemplateMissingApplyFails() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-4");
+ content.setTemplate("TestSimpleTemplate");
+ content.setRollback("DROP TABLE users");
+ // apply is NOT set
+
+ ValidationResult result = validator.validate(content);
+
+ assertTrue(result.hasErrors());
+ assertTrue(result.formatMessage().contains("SimpleTemplate requires 'apply' field"));
+ }
+ }
+
+ @Nested
+ @DisplayName("SteppableTemplate validation tests")
+ class SteppableTemplateValidationTests {
+
+ @Test
+ @DisplayName("SteppableTemplate with valid steps should pass validation")
+ void steppableTemplateWithValidStepsPasses() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-5");
+ content.setTemplate("TestSteppableTemplate");
+ content.setSteps(Arrays.asList(
+ createStep("CREATE TABLE users", null),
+ createStep("CREATE TABLE orders", "DROP TABLE orders")
+ ));
+
+ ValidationResult result = validator.validate(content);
+
+ assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage());
+ }
+
+ @Test
+ @DisplayName("SteppableTemplate with steps having apply and rollback should pass validation")
+ void steppableTemplateWithStepsHavingApplyAndRollbackPasses() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-6");
+ content.setTemplate("TestSteppableTemplate");
+ content.setSteps(Arrays.asList(
+ createStep("CREATE TABLE users", "DROP TABLE users"),
+ createStep("CREATE TABLE orders", "DROP TABLE orders")
+ ));
+
+ ValidationResult result = validator.validate(content);
+
+ assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage());
+ }
+
+ @Test
+ @DisplayName("SteppableTemplate with apply at root should fail validation")
+ void steppableTemplateWithApplyAtRootFails() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-7");
+ content.setTemplate("TestSteppableTemplate");
+ content.setApply("CREATE TABLE users"); // Should not be at root level
+ content.setSteps(Arrays.asList(createStep("step1", null)));
+
+ ValidationResult result = validator.validate(content);
+
+ assertTrue(result.hasErrors());
+ assertTrue(result.formatMessage().contains("SteppableTemplate must not have 'apply' at root level"));
+ }
+
+ @Test
+ @DisplayName("SteppableTemplate with rollback at root should fail validation")
+ void steppableTemplateWithRollbackAtRootFails() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-8");
+ content.setTemplate("TestSteppableTemplate");
+ content.setRollback("DROP TABLE users"); // Should not be at root level
+ content.setSteps(Arrays.asList(createStep("step1", null)));
+
+ ValidationResult result = validator.validate(content);
+
+ assertTrue(result.hasErrors());
+ assertTrue(result.formatMessage().contains("SteppableTemplate must not have 'rollback' at root level"));
+ }
+
+ @Test
+ @DisplayName("SteppableTemplate missing steps should fail validation")
+ void steppableTemplateMissingStepsFails() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-9");
+ content.setTemplate("TestSteppableTemplate");
+ // steps is NOT set
+
+ ValidationResult result = validator.validate(content);
+
+ assertTrue(result.hasErrors());
+ assertTrue(result.formatMessage().contains("SteppableTemplate requires 'steps' field"));
+ }
+
+ @Test
+ @DisplayName("SteppableTemplate with step missing apply should fail validation")
+ void steppableTemplateWithStepMissingApplyFails() {
+ ChangeTemplateFileContent content = new ChangeTemplateFileContent();
+ content.setId("test-change-10");
+ content.setTemplate("TestSteppableTemplate");
+
+ List