diff --git a/CLAUDE.md b/CLAUDE.md index d02fde92f..766fa14f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -296,9 +296,14 @@ Templates are **reusable, declarative change definitions** that enable "no-code - `ROLLBACK_FIELD` - Payload type for the rollback operation **Base Class**: `AbstractChangeTemplate` resolves generic types via reflection and provides: -- Field management: `changeId`, `isTransactional`, `configuration`, `applyPayload`, `rollbackPayload` +- Field management: `changeId`, `isTransactional`, `configuration` +- Generic type resolution for APPLY and ROLLBACK payload classes - Reflective class collection for GraalVM native image support +**Specialized Classes**: +- `AbstractSimpleTemplate`: For single-step changes with `setStep()`/`getStep()` +- `AbstractSteppableTemplate`: For multi-step changes with `setSteps()`/`getSteps()` + **Key Files**: - `core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java` - `core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java` 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 4526c8508..708223a70 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 @@ -20,10 +20,21 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Set; +/** + * Abstract base class for change templates providing common functionality. + * + *

This class handles generic type resolution and provides the common fields + * needed by all templates: changeId, isTransactional, and configuration. + * + *

For new templates, extend one of the specialized abstract classes: + *

+ */ public abstract class AbstractChangeTemplate implements ChangeTemplate { private final Class configurationClass; @@ -32,18 +43,6 @@ public abstract class AbstractChangeTemplate> stepsPayload; - private final Set> reflectiveClasses; @@ -94,34 +93,6 @@ public void setConfiguration(SHARED_CONFIGURATION_FIELD configuration) { this.configuration = configuration; } - /** - * @deprecated Use {@link #setStepsPayload(List)} instead. Will be removed in a future release. - */ - @Deprecated - @Override - public void setApplyPayload(APPLY_FIELD applyPayload) { - this.applyPayload = applyPayload; - } - - /** - * @deprecated Use {@link #setStepsPayload(List)} instead. Will be removed in a future release. - */ - @Deprecated - @Override - public void setRollbackPayload(ROLLBACK_FIELD rollbackPayload) { - this.rollbackPayload = rollbackPayload; - } - - @Override - public void setStepsPayload(List> stepsPayload) { - this.stepsPayload = stepsPayload; - } - - @Override - public List> getStepsPayload() { - return stepsPayload; - } - @Override public Class getConfigurationClass() { return configurationClass; diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java new file mode 100644 index 000000000..3642a9b35 --- /dev/null +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025 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; + +/** + * Abstract base class for templates with a single apply/rollback step. + * + *

Use this class when your template processes a single operation that may have + * an optional rollback. The YAML structure for this template type is: + * + *

{@code
+ * id: create-users-table
+ * template: SqlTemplate
+ * apply: "CREATE TABLE users ..."
+ * rollback: "DROP TABLE users"
+ * }
+ * + *

The framework will automatically create a {@link TemplateStep} from the + * apply/rollback fields in the YAML and inject it via {@link #setStep}. + * + * @param the type of shared configuration + * @param the type of the apply payload + * @param the type of the rollback payload + */ +public abstract class AbstractSimpleTemplate + extends AbstractChangeTemplate { + + protected TemplateStep step; + + public AbstractSimpleTemplate(Class... additionalReflectiveClass) { + super(additionalReflectiveClass); + } + + /** + * Sets the step containing the apply and optional rollback payloads. + * + * @param step the template step + */ + public void setStep(TemplateStep step) { + this.step = step; + } + + /** + * Returns the step containing the apply and optional rollback payloads. + * + * @return the template step, or null if not set + */ + public TemplateStep getStep() { + return step; + } + + /** + * Checks if this template has a step set. + * + * @return true if a step is set + */ + public boolean hasStep() { + return step != null; + } + + /** + * Convenience method to get the apply payload from the step. + * + * @return the apply payload, or null if no step is set + */ + public APPLY getApply() { + return step != null ? step.getApply() : null; + } + + /** + * Convenience method to get the rollback payload from the step. + * + * @return the rollback payload, or null if no step is set or no rollback defined + */ + public ROLLBACK getRollback() { + return step != null ? step.getRollback() : null; + } + + /** + * Checks if this template has a rollback payload defined. + * + * @return true if a step is set and it has a rollback payload + */ + public boolean hasRollback() { + return step != null && step.hasRollback(); + } +} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java new file mode 100644 index 000000000..310188031 --- /dev/null +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 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 java.util.List; + +/** + * Abstract base class for templates with multiple steps. + * + *

Use this class when your template processes multiple operations, each with + * its own apply and optional rollback. The YAML structure for this template type is: + * + *

{@code
+ * id: create-orders-collection
+ * template: MongoChangeTemplate
+ * steps:
+ *   - apply:
+ *       type: createCollection
+ *       collection: orders
+ *     rollback:
+ *       type: dropCollection
+ *       collection: orders
+ *   - apply:
+ *       type: insert
+ *       collection: orders
+ *       parameters:
+ *         documents:
+ *           - orderId: "ORD-001"
+ *     rollback:
+ *       type: delete
+ *       collection: orders
+ *       parameters:
+ *         filter: {}
+ * }
+ * + *

The framework will automatically parse the steps from the YAML and inject + * them via {@link #setSteps}. + * + *

Rollback Behavior: + *

    + *
  • When a step fails, all previously successful steps are rolled back in reverse order
  • + *
  • Steps without rollback operations are skipped during rollback
  • + *
  • Rollback errors are logged but don't stop the rollback process
  • + *
+ * + * @param the type of shared configuration + * @param the type of the apply payload for each step + * @param the type of the rollback payload for each step + */ +public abstract class AbstractSteppableTemplate + extends AbstractChangeTemplate { + + protected List> steps; + + public AbstractSteppableTemplate(Class... additionalReflectiveClass) { + super(additionalReflectiveClass); + } + + /** + * Sets the list of steps to execute. + * + * @param steps the list of template steps + */ + public void setSteps(List> steps) { + this.steps = steps; + } + + /** + * Returns the list of steps. + * + * @return the list of template steps, or null if not set + */ + public List> getSteps() { + return steps; + } + +} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java index fe9968f5c..c99e20d1f 100644 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java @@ -15,13 +15,17 @@ */ package io.flamingock.api.template; -import java.util.List; - /** * Interface representing a reusable change template with configuration of type {@code CONFIG}. * *

This interface is commonly implemented by classes that act as templates for Changes * where a specific configuration needs to be injected and managed independently. + * + *

Templates should extend one of the abstract base classes: + *

    + *
  • {@link AbstractSimpleTemplate} - for templates with a single apply/rollback step
  • + *
  • {@link AbstractSteppableTemplate} - for templates with multiple steps
  • + *
*/ public interface ChangeTemplate extends ReflectionMetadataProvider { @@ -31,26 +35,6 @@ public interface ChangeTemplate> stepsPayload); - - List> getStepsPayload(); - - default boolean hasStepsPayload() { - return getStepsPayload() != null && !getStepsPayload().isEmpty(); - } - Class getConfigurationClass(); Class getApplyPayloadClass(); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java index b556fa170..176b0e5f0 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java @@ -15,6 +15,8 @@ */ package io.flamingock.internal.core.task.executable; +import io.flamingock.api.template.AbstractSimpleTemplate; +import io.flamingock.api.template.AbstractSteppableTemplate; import io.flamingock.api.template.ChangeTemplate; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.ChangeExecutionException; @@ -51,49 +53,84 @@ protected void executeInternal(ExecutionRuntime executionRuntime, Method method ChangeTemplate changeTemplateInstance = (ChangeTemplate) instance; changeTemplateInstance.setTransactional(descriptor.isTransactional()); changeTemplateInstance.setChangeId(descriptor.getId()); - setExecutionData(executionRuntime, changeTemplateInstance, "Configuration"); - setExecutionData(executionRuntime, changeTemplateInstance, "ApplyPayload"); - setExecutionData(executionRuntime, changeTemplateInstance, "RollbackPayload"); - setStepsIfPresent(executionRuntime, changeTemplateInstance); + setConfigurationData(executionRuntime, changeTemplateInstance); + + if (instance instanceof AbstractSteppableTemplate) { + setStepsForSteppableTemplate(executionRuntime, (AbstractSteppableTemplate) instance); + } else if (instance instanceof AbstractSimpleTemplate) { + setStepForSimpleTemplate(executionRuntime, (AbstractSimpleTemplate) instance); + } + executionRuntime.executeMethodWithInjectedDependencies(instance, method); } catch (Throwable ex) { throw new ChangeExecutionException(ex.getMessage(), this.getId(), ex); } } + /** + * Sets the steps for an AbstractSteppableTemplate. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private void setStepsForSteppableTemplate(ExecutionRuntime executionRuntime, + AbstractSteppableTemplate instance) { + Object stepsData = descriptor.getSteps(); + + if (stepsData != null) { + logger.debug("Setting steps for steppable template change[{}]", descriptor.getId()); + + List convertedSteps = convertToTemplateSteps( + stepsData, + instance.getApplyPayloadClass(), + instance.getRollbackPayloadClass() + ); + + Method setStepsMethod = getSetterMethod(instance.getClass(), "setSteps"); + executionRuntime.executeMethodWithParameters(instance, setStepsMethod, convertedSteps); + } else { + logger.warn("No 'steps' section provided for steppable template-based change[{}]", descriptor.getId()); + } + } - private void setExecutionData(ExecutionRuntime executionRuntime, - ChangeTemplate instance, - String setterName) { - Class parameterClass; - Object data; - switch (setterName) { - case "Configuration": - parameterClass = instance.getConfigurationClass(); - data = descriptor.getConfiguration(); - break; - case "ApplyPayload": - parameterClass = instance.getApplyPayloadClass(); - data = descriptor.getApply(); - break; - case "RollbackPayload": - parameterClass = instance.getRollbackPayloadClass(); - data = descriptor.getRollback(); - break; - default: - throw new RuntimeException("Not found config setter for template: " + instance.getClass().getSimpleName()); + /** + * Sets the step for an AbstractSimpleTemplate. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private void setStepForSimpleTemplate(ExecutionRuntime executionRuntime, + AbstractSimpleTemplate instance) { + Object applyData = descriptor.getApply(); + + if (applyData != null) { + logger.debug("Setting step for simple template change[{}]", descriptor.getId()); + + TemplateStep step = convertToTemplateStep( + applyData, + descriptor.getRollback(), + instance.getApplyPayloadClass(), + instance.getRollbackPayloadClass() + ); + + Method setStepMethod = getSetterMethod(instance.getClass(), "setStep"); + executionRuntime.executeMethodWithParameters(instance, setStepMethod, step); + } else { + logger.warn("No 'apply' section provided for simple template-based change[{}]", descriptor.getId()); } - Method setConfigurationMethod = getSetterMethod(instance.getClass(), "set" + setterName); + } + + private void setConfigurationData(ExecutionRuntime executionRuntime, + ChangeTemplate instance) { + Class parameterClass = instance.getConfigurationClass(); + Object data = descriptor.getConfiguration(); - if(data != null && Void.class != parameterClass) { + if (data != null && Void.class != parameterClass) { + Method setConfigurationMethod = getSetterMethod(instance.getClass(), "setConfiguration"); executionRuntime.executeMethodWithParameters( instance, setConfigurationMethod, FileUtil.getFromMap(parameterClass, data)); - } else if(Void.class != parameterClass ) { - logger.warn("No '{}' section provided for template-based change[{}] of type[{}]", setterName, descriptor.getId(), descriptor.getTemplateClass().getName()); + } else if (Void.class != parameterClass) { + logger.warn("No 'Configuration' section provided for template-based change[{}] of type[{}]", + descriptor.getId(), descriptor.getTemplateClass().getName()); } - } @@ -107,27 +144,30 @@ private Method getSetterMethod(Class changeTemplateClass, String methodName) } /** - * Sets the steps on the template if steps data is present. - * Converts raw step data (List of Maps) to List of TemplateStep using the template's payload classes. + * Converts raw apply/rollback data from YAML to a TemplateStep object. + * + * @param applyData the raw apply data + * @param rollbackData the raw rollback data (maybe null) + * @param applyClass the class type for apply payload + * @param rollbackClass the class type for rollback payload + * @return the converted TemplateStep object */ @SuppressWarnings({"unchecked", "rawtypes"}) - private void setStepsIfPresent(ExecutionRuntime executionRuntime, - ChangeTemplate instance) { - Object stepsData = descriptor.getSteps(); - if (stepsData == null) { - return; + private TemplateStep convertToTemplateStep(Object applyData, + Object rollbackData, + Class applyClass, + Class rollbackClass) { + TemplateStep step = new TemplateStep(); + + if (applyData != null && Void.class != applyClass) { + step.setApply(FileUtil.getFromMap(applyClass, applyData)); } - logger.debug("Setting steps for change[{}]", descriptor.getId()); - - List convertedSteps = convertToTemplateSteps( - stepsData, - instance.getApplyPayloadClass(), - instance.getRollbackPayloadClass() - ); + if (rollbackData != null && Void.class != rollbackClass) { + step.setRollback(FileUtil.getFromMap(rollbackClass, rollbackData)); + } - Method setStepsMethod = getSetterMethod(instance.getClass(), "setStepsPayload"); - executionRuntime.executeMethodWithParameters(instance, setStepsMethod, convertedSteps); + return step; } /** diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java index 84d1ac984..ad14e6467 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java @@ -15,6 +15,7 @@ */ package io.flamingock.internal.core.task.executable.builder; +import io.flamingock.api.template.AbstractSteppableTemplate; import io.flamingock.internal.common.core.recovery.action.ChangeAction; import io.flamingock.internal.core.task.executable.ExecutableTask; import io.flamingock.internal.core.task.executable.TemplateExecutableTask; @@ -88,7 +89,15 @@ private TemplateExecutableTask buildTask(String stageName, TemplateLoadedChange loadedTask, ChangeAction action) { Method rollbackMethod = null; - if (loadedTask.getRollback() != null) { + + boolean isSteppableTemplate = AbstractSteppableTemplate.class.isAssignableFrom(loadedTask.getTemplateClass()); + + if (isSteppableTemplate) { + rollbackMethod = loadedTask.getRollbackMethod().orElse(null); + if (rollbackMethod != null) { + logger.trace("Change[{}] is a steppable template with rollback method", loadedTask.getId()); + } + } else if (loadedTask.getRollback() != null) { rollbackMethod = loadedTask.getRollbackMethod().orElse(null); if (rollbackMethod != null) { logger.trace("Change[{}] provides rollback in configuration", loadedTask.getId()); @@ -106,6 +115,7 @@ private TemplateExecutableTask buildTask(String stageName, ); } } + return new TemplateExecutableTask( stageName, loadedTask, @@ -115,4 +125,4 @@ private TemplateExecutableTask buildTask(String stageName, ); } -} \ No newline at end of file +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java similarity index 80% rename from core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java rename to core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java index 9fdb5b8b4..7d4bc20d3 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java @@ -17,8 +17,7 @@ import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.template.ChangeTemplateManager; -import io.flamingock.api.template.ChangeTemplate; -import io.flamingock.api.template.TemplateStep; +import io.flamingock.api.template.AbstractSimpleTemplate; import io.flamingock.api.annotations.Apply; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -26,55 +25,21 @@ import org.mockito.MockedStatic; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -class TemplateLoadedTaskBuilderTest { +class SimpleTemplateLoadedTaskBuilderTest { private TemplateLoadedTaskBuilder builder; - // Simple test template implementation - public static class TestChangeTemplate implements ChangeTemplate { + // Simple test template implementation using the abstract class + public static class TestChangeTemplate extends AbstractSimpleTemplate { - private List> stepsPayload; - - @Override - public void setChangeId(String changeId) {} - - @Override - public void setTransactional(boolean isTransactional) {} - - @Override - public void setConfiguration(Object configuration) {} - - @Override - public void setApplyPayload(Object applyPayload) {} - - @Override - public void setRollbackPayload(Object rollbackPayload) {} - - @Override - public void setStepsPayload(List> stepsPayload) { this.stepsPayload = stepsPayload; } - - @Override - public List> getStepsPayload() { return stepsPayload; } - - @Override - public Class getConfigurationClass() { return Object.class; } - - @Override - public Class getApplyPayloadClass() { return Object.class; } - - @Override - public Class getRollbackPayloadClass() { return Object.class; } - - @Override - public Collection> getReflectiveClasses() { return Collections.emptyList(); } + public TestChangeTemplate() { + super(); + } @Apply public void apply(Object config, Object execution, Object context) { 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 new file mode 100644 index 000000000..24262e1ff --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java @@ -0,0 +1,320 @@ +/* + * Copyright 2025 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.core.task.loaded; + +import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.template.ChangeTemplateManager; +import io.flamingock.api.template.AbstractSteppableTemplate; +import io.flamingock.api.annotations.Apply; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SteppableTemplateLoadedTaskBuilderTest { + + private TemplateLoadedTaskBuilder builder; + + // Steppable test template implementation using the abstract class + public static class TestSteppableTemplate extends AbstractSteppableTemplate { + + public TestSteppableTemplate() { + super(); + } + + @Apply + public void apply() { + // Test implementation - iterates through steps + } + } + + @BeforeEach + void setUp() { + builder = TemplateLoadedTaskBuilder.getInstance(); + } + + @Test + @DisplayName("Should build with steps when steps are provided") + void shouldBuildWithStepsWhenStepsProvided() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + List> rawSteps = Arrays.asList( + createStepMap("apply1", "rollback1"), + createStepMap("apply2", "rollback2") + ); + + builder.setId("test-id") + .setFileName("test-file.yml") + .setTemplateName("test-steppable-template") + .setTransactional(true) + .setSteps(rawSteps); + builder.setProfiles(Arrays.asList("test")); + + // When + TemplateLoadedChange result = builder.build(); + + // Then + assertNotNull(result); + assertEquals("test-id", result.getId()); + assertEquals("test-file.yml", result.getFileName()); + assertNotNull(result.getSteps()); + // Steps are stored as raw Object - conversion happens at execution time + assertEquals(rawSteps, result.getSteps()); + } + } + + @Test + @DisplayName("Should build with orderInContent when orderInContent is present for steppable template") + void shouldBuildWithOrderInContentForSteppableTemplate() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + List> rawSteps = Collections.singletonList( + createStepMap("apply1", "rollback1") + ); + + builder.setId("test-id") + .setOrder("001") + .setFileName("test-file.yml") + .setTemplateName("test-steppable-template") + .setRunAlways(false) + .setTransactional(true) + .setSystem(false) + .setConfiguration(new Object()) + .setSteps(rawSteps); + builder.setProfiles(Arrays.asList("test")); + + // When + TemplateLoadedChange result = builder.build(); + + // Then + assertEquals("001", result.getOrder().orElse(null)); + assertEquals("test-id", result.getId()); + assertEquals("test-file.yml", result.getFileName()); + } + } + + @Test + @DisplayName("Should build with order from fileName for steppable template") + void shouldBuildWithOrderFromFileNameForSteppableTemplate() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + List> rawSteps = Collections.singletonList( + createStepMap("apply1", "rollback1") + ); + + builder.setId("test-id") + .setOrder("0002") + .setFileName("_0002__test-file.yml") + .setTemplateName("test-steppable-template") + .setRunAlways(false) + .setTransactional(true) + .setSystem(false) + .setConfiguration(new Object()) + .setSteps(rawSteps); + builder.setProfiles(Arrays.asList("test")); + + // When + TemplateLoadedChange result = builder.build(); + + // Then + assertEquals("0002", result.getOrder().orElse(null)); + assertEquals("test-id", result.getId()); + assertEquals("_0002__test-file.yml", result.getFileName()); + } + } + + @Test + @DisplayName("Should build with empty steps list") + void shouldBuildWithEmptyStepsList() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + List> emptySteps = Collections.emptyList(); + + builder.setId("test-id") + .setFileName("test-file.yml") + .setTemplateName("test-steppable-template") + .setTransactional(true) + .setSteps(emptySteps); + builder.setProfiles(Arrays.asList("test")); + + // When + TemplateLoadedChange result = builder.build(); + + // Then + assertNotNull(result); + assertEquals("test-id", result.getId()); + assertNotNull(result.getSteps()); + assertTrue(((List) result.getSteps()).isEmpty()); + } + } + + @Test + @DisplayName("Should build with multiple steps preserving order") + void shouldBuildWithMultipleSteps() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + List> rawSteps = Arrays.asList( + createStepMap("createCollection", "dropCollection"), + createStepMap("insertDocument", "deleteDocument"), + createStepMap("createIndex", "dropIndex") + ); + + builder.setId("multi-step-change") + .setOrder("003") + .setFileName("_003__multi-step.yml") + .setTemplateName("test-steppable-template") + .setTransactional(true) + .setSteps(rawSteps); + builder.setProfiles(Arrays.asList("test")); + + // When + TemplateLoadedChange result = builder.build(); + + // Then + assertNotNull(result); + assertEquals("multi-step-change", result.getId()); + assertNotNull(result.getSteps()); + @SuppressWarnings("unchecked") + List> resultSteps = (List>) result.getSteps(); + assertEquals(3, resultSteps.size()); + // Verify steps are preserved in order + assertEquals("createCollection", resultSteps.get(0).get("apply")); + assertEquals("insertDocument", resultSteps.get(1).get("apply")); + assertEquals("createIndex", resultSteps.get(2).get("apply")); + } + } + + @Test + @DisplayName("Should throw exception when steppable template is not found") + void shouldThrowExceptionWhenSteppableTemplateNotFound() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("unknown-steppable-template")) + .thenReturn(Optional.empty()); + + List> rawSteps = Collections.singletonList( + createStepMap("apply1", "rollback1") + ); + + builder.setId("test-id") + .setOrder("001") + .setFileName("test-file.yml") + .setTemplateName("unknown-steppable-template") + .setSteps(rawSteps); + + // When & Then + FlamingockException exception = assertThrows(FlamingockException.class, () -> builder.build()); + + assertTrue(exception.getMessage().contains("Template[unknown-steppable-template] not found")); + } + } + + @Test + @DisplayName("Should build with steps having only apply (no rollback)") + void shouldBuildWithStepsHavingOnlyApply() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + List> rawSteps = Arrays.asList( + createStepMapApplyOnly("apply1"), + createStepMapApplyOnly("apply2") + ); + + builder.setId("test-id") + .setFileName("test-file.yml") + .setTemplateName("test-steppable-template") + .setTransactional(true) + .setSteps(rawSteps); + builder.setProfiles(Arrays.asList("test")); + + // When + TemplateLoadedChange result = builder.build(); + + // Then + assertNotNull(result); + @SuppressWarnings("unchecked") + List> resultSteps = (List>) result.getSteps(); + assertEquals(2, resultSteps.size()); + assertNull(resultSteps.get(0).get("rollback")); + assertNull(resultSteps.get(1).get("rollback")); + } + } + + @Test + @DisplayName("Should build steppable template with null steps") + void shouldBuildWithNullSteps() { + // Given + try (MockedStatic mockedTemplateManager = mockStatic(ChangeTemplateManager.class)) { + mockedTemplateManager.when(() -> ChangeTemplateManager.getTemplate("test-steppable-template")) + .thenReturn(Optional.of(TestSteppableTemplate.class)); + + builder.setId("test-id") + .setFileName("test-file.yml") + .setTemplateName("test-steppable-template") + .setTransactional(true) + .setSteps(null); + builder.setProfiles(Arrays.asList("test")); + + // When + TemplateLoadedChange result = builder.build(); + + // Then + assertNotNull(result); + assertEquals("test-id", result.getId()); + assertNull(result.getSteps()); + } + } + + private Map createStepMap(Object apply, Object rollback) { + Map step = new HashMap<>(); + step.put("apply", apply); + step.put("rollback", rollback); + return step; + } + + private Map createStepMapApplyOnly(Object apply) { + Map step = new HashMap<>(); + step.put("apply", apply); + return step; + } +} diff --git a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java index cab5ae879..47a128782 100644 --- a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java +++ b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java @@ -16,6 +16,8 @@ package io.flamingock.graalvm; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.AbstractSimpleTemplate; +import io.flamingock.api.template.AbstractSteppableTemplate; import io.flamingock.api.template.ChangeTemplate; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; @@ -143,6 +145,8 @@ private void registerTemplates() { registerClassForReflection(ChangeTemplateManager.class); registerClassForReflection(ChangeTemplate.class); registerClassForReflection(AbstractChangeTemplate.class); + registerClassForReflection(AbstractSimpleTemplate.class); + registerClassForReflection(AbstractSteppableTemplate.class); registerClassForReflection(TemplateStep.class); ChangeTemplateManager.getTemplates().forEach(template -> { registerClassForReflection(template.getClass()); 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 5a6058612..5cc9da350 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 @@ -18,7 +18,7 @@ import io.flamingock.api.annotations.Change; import io.flamingock.internal.common.core.template.ChangeTemplateFileContent; import io.flamingock.internal.common.core.task.RecoveryDescriptor; -import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.AbstractSimpleTemplate; import io.flamingock.internal.common.core.template.ChangeTemplateManager; import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import io.flamingock.internal.common.core.preview.builder.PreviewTaskBuilder; @@ -126,7 +126,11 @@ private AbstractLoadedTask getTemplateLoadedChange(String profiles) { } - public static abstract class TemplateSimulate implements ChangeTemplate {} + public static abstract class TemplateSimulate extends AbstractSimpleTemplate { + public TemplateSimulate() { + super(); + } + } @Change(id = "not-annotated", author = "aperezdieppa") public static class _000__NotAnnotated { @@ -146,4 +150,4 @@ public static class _002__NotP1 { @Change(id = "annotated-p1-p2", author = "aperezdieppa") public static class _003__P1AndP2 { } -} \ No newline at end of file +}