From 79b9f07a1d86b5a5510d7cfac8f82503c0ba499c Mon Sep 17 00:00:00 2001 From: Antonio Perez Dieppa Date: Fri, 20 Feb 2026 08:56:29 +0000 Subject: [PATCH 1/4] refactor: move template validation to loadedBuilder --- .../api/annotations/EnableFlamingock.java | 22 --- .../core/template/TemplateValidator.java | 91 +++++----- .../core/template/TemplateValidatorTest.java | 168 +++++++++++------- .../loaded/TemplateLoadedTaskBuilder.java | 29 ++- ...teppableTemplateLoadedTaskBuilderTest.java | 88 +++++++++ .../FlamingockAnnotationProcessor.java | 98 ---------- .../processor/PipelinePreProcessorTest.java | 5 - .../SpringProfileFilterTemplateTaskTest.java | 4 +- 8 files changed, 272 insertions(+), 233 deletions(-) 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 3c0b54ae0..f10058051 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 @@ -128,26 +128,4 @@ */ 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: - *

- *

- * SteppableTemplate validation: - *

- *

- * 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-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 index 055314921..4a15a0aab 100644 --- 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 @@ -18,6 +18,7 @@ import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.common.core.error.validation.ValidationResult; +import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import java.util.List; import java.util.Map; @@ -32,13 +33,8 @@ *

  • 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}: - *

    + *

    The validator is stateless and used at runtime during pipeline building to catch structural + * mismatches before any change execution begins. */ public class TemplateValidator { @@ -47,41 +43,37 @@ public class TemplateValidator { */ public enum TemplateType { /** - * Template annotated with {@code @ChangeTemplate(steppable = false)} or without annotation. + * Template annotated with {@code @ChangeTemplate(multiStep = false)} or default. * Uses apply/rollback fields. */ SIMPLE, /** - * Template annotated with {@code @ChangeTemplate(steppable = true)}. + * Template annotated with {@code @ChangeTemplate(multiStep = true)}. * Uses steps field. */ - STEPPABLE, - /** - * Template type could not be determined (kept for backward compatibility). - */ - UNKNOWN + STEPPABLE } private static final String ENTITY_TYPE = "template-change"; /** - * Creates a new TemplateValidator and ensures templates are loaded. + * Creates a new TemplateValidator. The validator is stateless. */ public TemplateValidator() { - ChangeTemplateManager.loadTemplates(); } /** - * Validates the YAML content structure against the template type. + * Validates the YAML content structure against the template type, performing + * template lookup from the registry. * - * @param content the parsed YAML content + * @param preview the template preview change to validate * @return ValidationResult containing any validation errors found */ - public ValidationResult validate(ChangeTemplateFileContent content) { + public ValidationResult validate(TemplatePreviewChange preview) { ValidationResult result = new ValidationResult("Template structure validation"); - String templateName = content.getTemplate(); - String changeId = content.getId() != null ? content.getId() : "unknown"; + String templateName = preview.getTemplateName(); + String changeId = preview.getId() != null ? preview.getId() : "unknown"; if (templateName == null || templateName.trim().isEmpty()) { result.add(new ValidationError("Template name is required", changeId, ENTITY_TYPE)); @@ -100,17 +92,41 @@ public ValidationResult validate(ChangeTemplateFileContent content) { } Class> templateClass = templateClassOpt.get(); - TemplateType type = getTemplateType(templateClass); + return validateStructure(templateClass, preview); + } + + /** + * Validates the YAML content structure against a resolved template class. + * This method is used by {@code TemplateLoadedTaskBuilder} which already has the template class + * resolved, avoiding a redundant lookup. + * + * @param templateClass the resolved template class + * @param preview the template preview change to validate + * @return ValidationResult containing any validation errors found + */ + public ValidationResult validateStructure(Class> templateClass, TemplatePreviewChange preview) { + ValidationResult result = new ValidationResult("Template structure validation"); + + String changeId = preview.getId() != null ? preview.getId() : "unknown"; + + ChangeTemplate annotation = templateClass.getAnnotation(ChangeTemplate.class); + if (annotation == null) { + result.add(new ValidationError( + "Template class '" + templateClass.getSimpleName() + "' is missing @ChangeTemplate annotation", + changeId, + ENTITY_TYPE + )); + return result; + } + + TemplateType type = annotation.multiStep() ? TemplateType.STEPPABLE : TemplateType.SIMPLE; switch (type) { case SIMPLE: - validateSimpleTemplate(content, changeId, result); + validateSimpleTemplate(preview, changeId, result); break; case STEPPABLE: - validateSteppableTemplate(content, changeId, result); - break; - case UNKNOWN: - // Unknown types are valid - they may have custom structure + validateSteppableTemplate(preview, changeId, result); break; } @@ -128,7 +144,6 @@ public TemplateType getTemplateType(Class stepList = (List) steps; for (int i = 0; i < stepList.size(); i++) { 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 index ba399f8e6..806ab2d13 100644 --- 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 @@ -19,6 +19,7 @@ import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.internal.common.core.error.validation.ValidationResult; +import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -49,7 +50,7 @@ public void apply() { } } - // Test template with @ChangeTemplate(steppable = true) + // Test template with @ChangeTemplate(multiStep = true) @ChangeTemplate(multiStep = true) public static class TestSteppableTemplate extends AbstractChangeTemplate { public TestSteppableTemplate() { @@ -62,11 +63,24 @@ public void apply() { } } + // Test template WITHOUT @ChangeTemplate annotation + public static class TestUnannotatedTemplate extends AbstractChangeTemplate { + public TestUnannotatedTemplate() { + super(); + } + + @Apply + public void apply() { + // Test implementation + } + } + @BeforeEach void setUp() { // Register test templates ChangeTemplateManager.addTemplate("TestSimpleTemplate", TestSimpleTemplate.class); ChangeTemplateManager.addTemplate("TestSteppableTemplate", TestSteppableTemplate.class); + ChangeTemplateManager.addTemplate("TestUnannotatedTemplate", TestUnannotatedTemplate.class); validator = new TemplateValidator(); } @@ -96,12 +110,10 @@ 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"); + TemplatePreviewChange preview = createPreview("test-change-1", "TestSimpleTemplate"); + preview.setApply("CREATE TABLE users"); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage()); } @@ -109,13 +121,11 @@ void simpleTemplateWithApplyOnlyPasses() { @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"); + TemplatePreviewChange preview = createPreview("test-change-2", "TestSimpleTemplate"); + preview.setApply("CREATE TABLE users"); + preview.setRollback("DROP TABLE users"); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage()); } @@ -123,13 +133,11 @@ void simpleTemplateWithApplyAndRollbackPasses() { @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))); + TemplatePreviewChange preview = createPreview("test-change-3", "TestSimpleTemplate"); + preview.setApply("CREATE TABLE users"); + preview.setSteps(Arrays.asList(createStep("step1", null))); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("SimpleTemplate must not have 'steps' field")); @@ -138,13 +146,11 @@ void simpleTemplateWithStepsFails() { @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"); + TemplatePreviewChange preview = createPreview("test-change-4", "TestSimpleTemplate"); + preview.setRollback("DROP TABLE users"); // apply is NOT set - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("SimpleTemplate requires 'apply' field")); @@ -158,15 +164,13 @@ 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( + TemplatePreviewChange preview = createPreview("test-change-5", "TestSteppableTemplate"); + preview.setSteps(Arrays.asList( createStep("CREATE TABLE users", null), createStep("CREATE TABLE orders", "DROP TABLE orders") )); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage()); } @@ -174,15 +178,13 @@ void steppableTemplateWithValidStepsPasses() { @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( + TemplatePreviewChange preview = createPreview("test-change-6", "TestSteppableTemplate"); + preview.setSteps(Arrays.asList( createStep("CREATE TABLE users", "DROP TABLE users"), createStep("CREATE TABLE orders", "DROP TABLE orders") )); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage()); } @@ -190,13 +192,11 @@ void steppableTemplateWithStepsHavingApplyAndRollbackPasses() { @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))); + TemplatePreviewChange preview = createPreview("test-change-7", "TestSteppableTemplate"); + preview.setApply("CREATE TABLE users"); // Should not be at root level + preview.setSteps(Arrays.asList(createStep("step1", null))); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("SteppableTemplate must not have 'apply' at root level")); @@ -205,13 +205,11 @@ void steppableTemplateWithApplyAtRootFails() { @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))); + TemplatePreviewChange preview = createPreview("test-change-8", "TestSteppableTemplate"); + preview.setRollback("DROP TABLE users"); // Should not be at root level + preview.setSteps(Arrays.asList(createStep("step1", null))); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("SteppableTemplate must not have 'rollback' at root level")); @@ -220,12 +218,10 @@ void steppableTemplateWithRollbackAtRootFails() { @Test @DisplayName("SteppableTemplate missing steps should fail validation") void steppableTemplateMissingStepsFails() { - ChangeTemplateFileContent content = new ChangeTemplateFileContent(); - content.setId("test-change-9"); - content.setTemplate("TestSteppableTemplate"); + TemplatePreviewChange preview = createPreview("test-change-9", "TestSteppableTemplate"); // steps is NOT set - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("SteppableTemplate requires 'steps' field")); @@ -234,17 +230,15 @@ void steppableTemplateMissingStepsFails() { @Test @DisplayName("SteppableTemplate with step missing apply should fail validation") void steppableTemplateWithStepMissingApplyFails() { - ChangeTemplateFileContent content = new ChangeTemplateFileContent(); - content.setId("test-change-10"); - content.setTemplate("TestSteppableTemplate"); + TemplatePreviewChange preview = createPreview("test-change-10", "TestSteppableTemplate"); List> steps = new ArrayList<>(); Map step1 = new HashMap<>(); step1.put("rollback", "DROP TABLE users"); // apply is missing steps.add(step1); - content.setSteps(steps); + preview.setSteps(steps); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("Step 1 is missing required 'apply' field")); @@ -258,12 +252,10 @@ class TemplateNotFoundTests { @Test @DisplayName("Unknown template name should fail with template not found error") void unknownTemplateNameFails() { - ChangeTemplateFileContent content = new ChangeTemplateFileContent(); - content.setId("test-change-11"); - content.setTemplate("UnknownTemplate"); - content.setApply("some operation"); + TemplatePreviewChange preview = createPreview("test-change-11", "UnknownTemplate"); + preview.setApply("some operation"); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("Template 'UnknownTemplate' not found")); @@ -272,18 +264,66 @@ void unknownTemplateNameFails() { @Test @DisplayName("Missing template name should fail validation") void missingTemplateNameFails() { - ChangeTemplateFileContent content = new ChangeTemplateFileContent(); - content.setId("test-change-12"); - // template is NOT set - content.setApply("some operation"); + TemplatePreviewChange preview = createPreview("test-change-12", null); + preview.setApply("some operation"); - ValidationResult result = validator.validate(content); + ValidationResult result = validator.validate(preview); assertTrue(result.hasErrors()); assertTrue(result.formatMessage().contains("Template name is required")); } } + @Nested + @DisplayName("validateStructure tests") + class ValidateStructureTests { + + @Test + @DisplayName("Should validate structure correctly with resolved template class") + void shouldValidateStructureWithResolvedClass() { + TemplatePreviewChange preview = createPreview("test-change-13", "TestSimpleTemplate"); + preview.setApply("CREATE TABLE users"); + + ValidationResult result = validator.validateStructure(TestSimpleTemplate.class, preview); + + assertFalse(result.hasErrors(), "Should have no errors: " + result.formatMessage()); + } + + @Test + @DisplayName("Should fail when template class is missing @ChangeTemplate annotation") + void shouldFailWhenMissingAnnotation() { + TemplatePreviewChange preview = createPreview("test-change-14", "TestUnannotatedTemplate"); + preview.setApply("some operation"); + + ValidationResult result = validator.validateStructure(TestUnannotatedTemplate.class, preview); + + assertTrue(result.hasErrors()); + assertTrue(result.formatMessage().contains("missing @ChangeTemplate annotation")); + } + + @Test + @DisplayName("Should detect steppable template structure mismatch via validateStructure") + void shouldDetectSteppableMismatchViaValidateStructure() { + TemplatePreviewChange preview = createPreview("test-change-15", "TestSteppableTemplate"); + preview.setApply("CREATE TABLE users"); // Wrong for steppable + + ValidationResult result = validator.validateStructure(TestSteppableTemplate.class, preview); + + assertTrue(result.hasErrors()); + assertTrue(result.formatMessage().contains("SteppableTemplate must not have 'apply' at root level")); + } + } + + /** + * Helper to create a TemplatePreviewChange with id and template name. + */ + private TemplatePreviewChange createPreview(String id, String templateName) { + TemplatePreviewChange preview = new TemplatePreviewChange(); + preview.setId(id); + preview.setSource(templateName); + return preview; + } + /** * Helper method to create a step map with apply and optional rollback. */ diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java index 5ad6831e5..f581728d5 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java @@ -19,11 +19,13 @@ import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.error.validation.ValidationResult; import io.flamingock.internal.common.core.preview.AbstractPreviewTask; import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.internal.common.core.task.TargetSystemDescriptor; import io.flamingock.internal.common.core.template.ChangeTemplateManager; +import io.flamingock.internal.common.core.template.TemplateValidator; import io.flamingock.internal.util.FileUtil; import io.flamingock.internal.util.Pair; import io.flamingock.internal.util.ReflectionUtil; @@ -38,6 +40,8 @@ //TODO how to set transactional and runAlways public class TemplateLoadedTaskBuilder implements LoadedTaskBuilder> { + private static final TemplateValidator DEFAULT_VALIDATOR = new TemplateValidator(); + private String fileName; private String id; private String order; @@ -53,18 +57,33 @@ public class TemplateLoadedTaskBuilder implements LoadedTaskBuilder> templateClass = ChangeTemplateManager.getTemplate(templateName) .orElseThrow(()-> new FlamingockException(String.format("Template[%s] not found. This is probably because template's name is wrong or template's library not imported", templateName))); + if (preview != null) { + ValidationResult validationResult = templateValidator.validateStructure(templateClass, preview); + if (validationResult.hasErrors()) { + throw new FlamingockException( + "Template structure validation failed for change '" + id + "':\n" + validationResult.formatMessage()); + } + } + Constructor constructor = ReflectionUtil.getDefaultConstructor(templateClass); // Determine template type based on @ChangeTemplate annotation @@ -301,7 +328,7 @@ private List> convertSteps(Constructor construct } private TemplateLoadedTaskBuilder setPreview(TemplatePreviewChange preview) { - + this.preview = preview; setFileName(preview.getFileName()); setId(preview.getId()); setOrder(preview.getOrder().orElse(null)); diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java index 86eb18e0d..fd93323ab 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java @@ -20,9 +20,12 @@ import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import io.flamingock.internal.common.core.template.ChangeTemplateManager; +import io.flamingock.internal.common.core.template.TemplateValidator; 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 org.mockito.MockedStatic; @@ -54,6 +57,20 @@ public void apply() { } } + // Simple test template implementation + @ChangeTemplate + public static class TestSimpleTemplate extends AbstractChangeTemplate { + + public TestSimpleTemplate() { + super(); + } + + @Apply + public void apply() { + // Test implementation + } + } + @BeforeEach void setUp() { builder = TemplateLoadedTaskBuilder.getInstance(); @@ -336,4 +353,75 @@ private Map createStepMapApplyOnly(Object apply) { step.put("apply", apply); return step; } + + @Nested + @DisplayName("Template structure validation tests") + class TemplateStructureValidationTests { + + @Test + @DisplayName("Steppable template with apply at root level should throw FlamingockException during build") + void steppableTemplateWithApplyAtRootShouldThrow() { + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + TemplatePreviewChange preview = new TemplatePreviewChange(); + preview.setId("bad-steppable"); + preview.setSource("test-steppable-template"); + preview.setFileName("test-file.yml"); + preview.setApply("CREATE TABLE users"); // Wrong: root-level apply on steppable + + TemplateLoadedTaskBuilder validatingBuilder = TemplateLoadedTaskBuilder.getInstanceFromPreview(preview); + + FlamingockException exception = assertThrows(FlamingockException.class, validatingBuilder::build); + assertTrue(exception.getMessage().contains("Template structure validation failed")); + assertTrue(exception.getMessage().contains("SteppableTemplate must not have 'apply' at root level")); + } + } + + @Test + @DisplayName("Simple template with steps field should throw FlamingockException during build") + void simpleTemplateWithStepsShouldThrow() { + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-simple-template")) + .thenReturn(Optional.of(TestSimpleTemplate.class)); + + TemplatePreviewChange preview = new TemplatePreviewChange(); + preview.setId("bad-simple"); + preview.setSource("test-simple-template"); + preview.setFileName("test-file.yml"); + preview.setApply("CREATE TABLE users"); + preview.setSteps(Arrays.asList(createStepMap("step1", null))); // Wrong: steps on simple + + TemplateLoadedTaskBuilder validatingBuilder = TemplateLoadedTaskBuilder.getInstanceFromPreview(preview); + + FlamingockException exception = assertThrows(FlamingockException.class, validatingBuilder::build); + assertTrue(exception.getMessage().contains("Template structure validation failed")); + assertTrue(exception.getMessage().contains("SimpleTemplate must not have 'steps' field")); + } + } + + @Test + @DisplayName("Custom TemplateValidator injection should work via getInstance") + void customValidatorInjectionShouldWork() { + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + TemplateValidator customValidator = new TemplateValidator(); + + TemplatePreviewChange preview = new TemplatePreviewChange(); + preview.setId("custom-validator-test"); + preview.setSource("test-steppable-template"); + preview.setFileName("test-file.yml"); + preview.setSteps(Arrays.asList(createStepMap("apply1", "rollback1"))); + + TemplateLoadedTaskBuilder validatingBuilder = TemplateLoadedTaskBuilder.getInstanceFromPreview(preview, customValidator); + + AbstractTemplateLoadedChange result = validatingBuilder.build(); + assertInstanceOf(SteppableTemplateLoadedChange.class, result); + assertEquals("custom-validator-test", result.getId()); + } + } + } } diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java index 69a416d00..cdfebe141 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java @@ -26,17 +26,12 @@ import io.flamingock.internal.common.core.metadata.BuilderProviderInfo; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.pipeline.PipelineHelper; -import io.flamingock.internal.common.core.error.validation.ValidationError; -import io.flamingock.internal.common.core.error.validation.ValidationResult; import io.flamingock.internal.common.core.preview.AbstractPreviewTask; import io.flamingock.internal.common.core.preview.CodePreviewChange; import io.flamingock.internal.common.core.preview.PreviewPipeline; import io.flamingock.internal.common.core.preview.PreviewStage; import io.flamingock.internal.common.core.preview.SystemPreviewStage; -import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import io.flamingock.internal.common.core.task.TaskDescriptor; -import io.flamingock.internal.common.core.template.ChangeTemplateFileContent; -import io.flamingock.internal.common.core.template.TemplateValidator; import io.flamingock.internal.common.core.util.LoggerPreProcessor; import io.flamingock.internal.common.core.util.Serializer; import org.jetbrains.annotations.NotNull; @@ -233,7 +228,6 @@ public boolean process(Set annotations, RoundEnvironment ); validateAllChangesAreMappedToStages(standardChangesMapByPackage, pipeline, flamingockAnnotation.strictStageMapping()); - validateTemplateStructures(pipeline, flamingockAnnotation.strictTemplateValidation()); Serializer serializer = new Serializer(processingEnv, logger); String configFile = flamingockAnnotation.configFile(); @@ -777,96 +771,4 @@ private void validateAllChangesAreMappedToStages(Map allErrors = new ArrayList<>(); - - // Collect errors from system stage - if (pipeline.getSystemStage() != null && pipeline.getSystemStage().getTasks() != null) { - collectTemplateValidationErrors(validator, pipeline.getSystemStage().getTasks(), allErrors); - } - - // Collect errors from regular stages - if (pipeline.getStages() != null) { - for (PreviewStage stage : pipeline.getStages()) { - if (stage.getTasks() != null) { - collectTemplateValidationErrors(validator, stage.getTasks(), allErrors); - } - } - } - - if (!allErrors.isEmpty()) { - String message = formatTemplateValidationErrors(allErrors); - if (Boolean.TRUE.equals(strictTemplateValidation)) { - throw new RuntimeException(message); - } else { - logger.warn(message); - } - } - } - - /** - * Collects validation errors from template-based tasks. - * - * @param validator the template validator - * @param tasks the collection of preview tasks to check - * @param errors the list to add validation errors to - */ - private void collectTemplateValidationErrors( - TemplateValidator validator, - Collection tasks, - List errors) { - - for (AbstractPreviewTask task : tasks) { - if (task instanceof TemplatePreviewChange) { - TemplatePreviewChange templateTask = (TemplatePreviewChange) task; - - // Build ChangeTemplateFileContent from preview for validation - ChangeTemplateFileContent content = toFileContent(templateTask); - - ValidationResult result = validator.validate(content); - if (result.hasErrors()) { - errors.addAll(result.getErrors()); - } - } - } - } - - /** - * Converts a TemplatePreviewChange to ChangeTemplateFileContent for validation. - * - * @param preview the template preview change - * @return the file content representation - */ - private ChangeTemplateFileContent toFileContent(TemplatePreviewChange preview) { - ChangeTemplateFileContent content = new ChangeTemplateFileContent(); - content.setId(preview.getId()); - content.setTemplate(preview.getTemplateName()); - content.setApply(preview.getApply()); - content.setRollback(preview.getRollback()); - content.setSteps(preview.getSteps()); - return content; - } - - /** - * Formats template validation errors into a readable message. - * - * @param errors the list of validation errors - * @return formatted error message - */ - private String formatTemplateValidationErrors(List errors) { - StringBuilder sb = new StringBuilder("Template structure validation errors:\n"); - for (ValidationError error : errors) { - sb.append(" - ").append(error.getFormattedMessage()).append("\n"); - } - return sb.toString(); - } } diff --git a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java index acfe10051..7a2b0d8bd 100644 --- a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java +++ b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java @@ -513,11 +513,6 @@ public boolean strictStageMapping() { return true; } - @Override - public boolean strictTemplateValidation() { - return true; - } - @Override public Class annotationType() { return EnableFlamingock.class; } }; } diff --git a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java index 60c477696..7aaabffab 100644 --- a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java +++ b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java @@ -115,7 +115,7 @@ private AbstractLoadedTask getTemplateLoadedChange(String profiles) { profiles, true, null, - null, + "dummy-apply", null, null, RecoveryDescriptor.getDefault() @@ -128,7 +128,7 @@ private AbstractLoadedTask getTemplateLoadedChange(String profiles) { } @ChangeTemplate - public static abstract class TemplateSimulate extends AbstractChangeTemplate { + public static class TemplateSimulate extends AbstractChangeTemplate { public TemplateSimulate() { super(); } From b4924bbae34ec1b20b7ebba272630c3a35eff819 Mon Sep 17 00:00:00 2001 From: Antonio Perez Dieppa Date: Fri, 20 Feb 2026 10:16:54 +0000 Subject: [PATCH 2/4] refactor: move template validation to loadedBuilder --- .../template/ChangeTemplateDefinition.java | 53 +++++++++ .../core/template/ChangeTemplateFactory.java | 2 +- .../core/template/ChangeTemplateManager.java | 64 ++++++++--- .../core/template/TemplateValidator.java | 55 ++++----- .../template/ChangeTemplateManagerTest.java | 104 ++++++++++++++++++ .../core/template/TemplateValidatorTest.java | 56 ++-------- .../loaded/TemplateLoadedTaskBuilder.java | 18 +-- .../SimpleTemplateLoadedTaskBuilderTest.java | 7 +- ...teppableTemplateLoadedTaskBuilderTest.java | 21 ++-- .../graalvm/RegistrationFeature.java | 2 +- 10 files changed, 259 insertions(+), 123 deletions(-) create mode 100644 core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateDefinition.java create mode 100644 core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateDefinition.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateDefinition.java new file mode 100644 index 000000000..f9406ccfa --- /dev/null +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateDefinition.java @@ -0,0 +1,53 @@ +/* + * 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.ChangeTemplate; + +/** + * Wraps a template class together with its pre-resolved metadata from the {@code @ChangeTemplate} annotation. + *

    + * Created at registration time in {@link ChangeTemplateManager}, this ensures: + *

    + */ +public class ChangeTemplateDefinition { + + private final Class> templateClass; + private final boolean multiStep; + + public ChangeTemplateDefinition( + Class> templateClass, + boolean multiStep) { + this.templateClass = templateClass; + this.multiStep = multiStep; + } + + public String getId() { + return getTemplateClass().getSimpleName(); + } + + public Class> getTemplateClass() { + return templateClass; + } + + public boolean isMultiStep() { + return multiStep; + } +} diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java index 42fa137f1..80fdab850 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java @@ -47,7 +47,7 @@ public interface ChangeTemplateFactory { /** * Returns a collection of {@link ChangeTemplate} instances provided by this factory. *

    - * This method is called by {@link ChangeTemplateManager#getTemplates()} to discover templates + * This method is called by {@link ChangeTemplateManager#getRawTemplates()} to discover templates * in a federated manner. It is invoked in two contexts: *