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 1a278531d..2ef86b3f8 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 @@ -39,7 +39,7 @@ * @param rollback payload type * @see io.flamingock.api.annotations.ChangeTemplate */ -public abstract class AbstractChangeTemplate implements ChangeTemplate { +public abstract class AbstractChangeTemplate implements ChangeTemplate { private final Class configurationClass; private final Class applyPayloadClass; 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 cdc0cbde0..f83bae4df 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 @@ -28,7 +28,7 @@ * @see AbstractChangeTemplate * @see io.flamingock.api.annotations.ChangeTemplate */ -public interface ChangeTemplate extends ReflectionMetadataProvider { +public interface ChangeTemplate extends ReflectionMetadataProvider { void setChangeId(String changeId); diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplatePayload.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplatePayload.java new file mode 100644 index 000000000..39abb37f9 --- /dev/null +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplatePayload.java @@ -0,0 +1,34 @@ +/* + * 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 java.util.List; + +/** + * Contract for template payload types (APPLY and ROLLBACK generics). + * + *

All template payload types must implement this interface to enable + * structural validation at pipeline load time, before any change executes. + */ +public interface TemplatePayload { + + /** + * Validates this payload and returns any errors found. + * + * @return list of validation errors, empty if payload is valid + */ + List validate(); +} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplatePayloadValidationError.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplatePayloadValidationError.java new file mode 100644 index 000000000..8ea614710 --- /dev/null +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplatePayloadValidationError.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.api.template; + +/** + * Represents a validation error found in a {@link TemplatePayload}. + * + *

Errors can be field-level (with a specific field name) or general (without a field). + */ +public class TemplatePayloadValidationError { + + private final String field; + private final String message; + + public TemplatePayloadValidationError(String message) { + this(null, message); + } + + public TemplatePayloadValidationError(String field, String message) { + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } + + public String getFormattedMessage() { + return field != null ? String.format("[field: %s] %s", field, message) : message; + } + + @Override + public String toString() { + return getFormattedMessage(); + } +} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/wrappers/TemplateString.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/wrappers/TemplateString.java new file mode 100644 index 000000000..5644a0894 --- /dev/null +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/wrappers/TemplateString.java @@ -0,0 +1,86 @@ +/* + * 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.wrappers; + +import io.flamingock.api.template.TemplatePayload; +import io.flamingock.api.template.TemplatePayloadValidationError; + +import java.util.Collections; +import java.util.List; + +/** + * A {@link TemplatePayload} wrapper for {@code String} payloads. + * + *

Provides a drop-in replacement for raw {@code String} payloads in templates, + * keeping YAML clean while satisfying the {@code TemplatePayload} contract. + * + *

Supports SnakeYAML deserialization via the no-arg constructor and scalar + * conversion via the {@code String} constructor. + */ +public class TemplateString implements TemplatePayload { + + private String value; + + /** + * No-arg constructor for SnakeYAML deserialization. + */ + public TemplateString() { + } + + /** + * Constructor for scalar conversion (e.g., from YAML string values). + * + * @param value the string value + */ + public TemplateString(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public List validate() { + if (value == null || value.trim().isEmpty()) { + return Collections.singletonList( + new TemplatePayloadValidationError("value", "must not be null or blank")); + } + return Collections.emptyList(); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TemplateString that = (TemplateString) o; + return value != null ? value.equals(that.value) : that.value == null; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } +} 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 index 7d1a60a48..386664c59 100644 --- 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 @@ -18,10 +18,13 @@ import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.template.wrappers.TemplateString; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.Collection; +import java.util.Collections; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -33,13 +36,23 @@ public static class TestConfig { } // Simple test apply payload class - public static class TestApplyPayload { + public static class TestApplyPayload implements TemplatePayload { public String applyData; + + @Override + public List validate() { + return Collections.emptyList(); + } } // Simple test rollback payload class - public static class TestRollbackPayload { + public static class TestRollbackPayload implements TemplatePayload { public String rollbackData; + + @Override + public List validate() { + return Collections.emptyList(); + } } // Additional class for reflection @@ -93,7 +106,7 @@ public void rollback() { // Test template with Void configuration @ChangeTemplate(name = "test-template-with-void-config") public static class TestTemplateWithVoidConfig - extends AbstractChangeTemplate { + extends AbstractChangeTemplate { public TestTemplateWithVoidConfig() { super(); @@ -191,8 +204,8 @@ void getReflectiveClassesWithVoidConfigShouldIncludeVoidClass() { assertTrue(reflectiveClasses.contains(Void.class), "Should contain Void class for configuration"); - assertTrue(reflectiveClasses.contains(String.class), - "Should contain String class for apply/rollback payloads"); + assertTrue(reflectiveClasses.contains(TemplateString.class), + "Should contain TemplateString class for apply/rollback payloads"); } @Test diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java index 56446e0e0..ceba61ab0 100644 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java @@ -19,6 +19,7 @@ import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.wrappers.TemplateString; import io.flamingock.internal.common.core.error.FlamingockException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -30,7 +31,7 @@ class ChangeTemplateManagerTest { @ChangeTemplate(name = "annotated-simple-template") - public static class AnnotatedSimpleTemplate extends AbstractChangeTemplate { + public static class AnnotatedSimpleTemplate extends AbstractChangeTemplate { public AnnotatedSimpleTemplate() { super(); } @@ -45,7 +46,7 @@ public void rollback() { } @ChangeTemplate(name = "annotated-steppable-template", multiStep = true) - public static class AnnotatedSteppableTemplate extends AbstractChangeTemplate { + public static class AnnotatedSteppableTemplate extends AbstractChangeTemplate { public AnnotatedSteppableTemplate() { super(); } @@ -59,7 +60,7 @@ public void rollback() { } } - public static class UnannotatedTemplate extends AbstractChangeTemplate { + public static class UnannotatedTemplate extends AbstractChangeTemplate { public UnannotatedTemplate() { super(); } @@ -113,7 +114,7 @@ void getTemplateForUnregisteredNameShouldReturnEmpty() { } @ChangeTemplate(name = "template-rollback-not-required", rollbackPayloadRequired = false) - public static class TemplateWithRollbackNotRequired extends AbstractChangeTemplate { + public static class TemplateWithRollbackNotRequired extends AbstractChangeTemplate { public TemplateWithRollbackNotRequired() { super(); } @@ -140,7 +141,7 @@ void addTemplateWithRollbackPayloadRequiredFalseShouldPropagate() { } @ChangeTemplate(name = "template-without-rollback") - public static class TemplateWithoutRollback extends AbstractChangeTemplate { + public static class TemplateWithoutRollback extends AbstractChangeTemplate { public TemplateWithoutRollback() { super(); } 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 2fdd170af..f973bfd92 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.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.wrappers.TemplateString; import io.flamingock.internal.common.core.error.validation.ValidationResult; import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import org.junit.jupiter.api.BeforeEach; @@ -40,7 +41,7 @@ class TemplateValidatorTest { // Test template with @ChangeTemplate (simple template) @ChangeTemplate(name = "test-simple-template") - public static class TestSimpleTemplate extends AbstractChangeTemplate { + public static class TestSimpleTemplate extends AbstractChangeTemplate { public TestSimpleTemplate() { super(); } @@ -57,7 +58,7 @@ public void rollback() { // Test template with @ChangeTemplate(multiStep = true) @ChangeTemplate(name = "test-steppable-template", multiStep = true) - public static class TestSteppableTemplate extends AbstractChangeTemplate { + public static class TestSteppableTemplate extends AbstractChangeTemplate { public TestSteppableTemplate() { super(); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractTemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractTemplateExecutableTask.java index d6641cfb1..0b5729ec9 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractTemplateExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractTemplateExecutableTask.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.task.executable; import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.TemplatePayload; import io.flamingock.internal.common.core.recovery.action.ChangeAction; import io.flamingock.internal.core.task.loaded.AbstractTemplateLoadedChange; import io.flamingock.internal.util.FileUtil; @@ -35,7 +36,7 @@ * @param the rollback payload type * @param the type of template loaded change */ -public abstract class AbstractTemplateExecutableTask> extends ReflectionExecutableTask { protected final Logger logger = FlamingockLoggerFactory.getLogger("TemplateTask"); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SimpleTemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SimpleTemplateExecutableTask.java index 5add1dad2..cf804ea7c 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SimpleTemplateExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SimpleTemplateExecutableTask.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.task.executable; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplatePayload; import io.flamingock.internal.common.core.error.ChangeExecutionException; import io.flamingock.internal.common.core.recovery.action.ChangeAction; import io.flamingock.internal.core.runtime.ExecutionRuntime; @@ -31,7 +32,8 @@ * @param the apply payload type * @param the rollback payload type */ -public class SimpleTemplateExecutableTask +@SuppressWarnings({"rawtypes", "unchecked"}) +public class SimpleTemplateExecutableTask extends AbstractTemplateExecutableTask> { @@ -56,10 +58,9 @@ public void rollback(ExecutionRuntime executionRuntime) { executeInternal(executionRuntime, rollbackMethod); } - @SuppressWarnings("unchecked") protected void executeInternal(ExecutionRuntime executionRuntime, Method method) { try { - AbstractChangeTemplate instance = (AbstractChangeTemplate) + AbstractChangeTemplate instance = (AbstractChangeTemplate) executionRuntime.getInstance(descriptor.getConstructor()); instance.setTransactional(descriptor.isTransactional()); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTask.java index 893eb918c..455315fd3 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTask.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.task.executable; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplatePayload; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.ChangeExecutionException; import io.flamingock.internal.common.core.recovery.action.ChangeAction; @@ -34,7 +35,8 @@ * @param the apply payload type * @param the rollback payload type */ -public class SteppableTemplateExecutableTask +@SuppressWarnings({"rawtypes", "unchecked"}) +public class SteppableTemplateExecutableTask extends AbstractTemplateExecutableTask> { @@ -50,7 +52,7 @@ public SteppableTemplateExecutableTask(String stageName, @Override public void apply(ExecutionRuntime executionRuntime) { - AbstractChangeTemplate instance = buildInstance(executionRuntime); + AbstractChangeTemplate instance = buildInstance(executionRuntime); try { List> steps = descriptor.getSteps(); @@ -67,7 +69,7 @@ public void apply(ExecutionRuntime executionRuntime) { @Override public void rollback(ExecutionRuntime executionRuntime) { - AbstractChangeTemplate instance = buildInstance(executionRuntime); + AbstractChangeTemplate instance = buildInstance(executionRuntime); try { List> steps = descriptor.getSteps(); @@ -88,14 +90,13 @@ public void rollback(ExecutionRuntime executionRuntime) { } @NotNull - @SuppressWarnings("unchecked") - private AbstractChangeTemplate buildInstance(ExecutionRuntime executionRuntime) { - AbstractChangeTemplate instance; + private AbstractChangeTemplate buildInstance(ExecutionRuntime executionRuntime) { + AbstractChangeTemplate instance; try { logger.debug("Starting execution of change[{}] with template: {}", descriptor.getId(), descriptor.getTemplateClass()); logger.debug("change[{}] transactional: {}", descriptor.getId(), descriptor.isTransactional()); - instance = (AbstractChangeTemplate) + instance = (AbstractChangeTemplate) executionRuntime.getInstance(descriptor.getConstructor()); instance.setTransactional(descriptor.isTransactional()); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractTemplateLoadedChange.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractTemplateLoadedChange.java index b4bb260f0..2ba8d0d86 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractTemplateLoadedChange.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractTemplateLoadedChange.java @@ -18,6 +18,7 @@ import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.TemplatePayload; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.internal.common.core.task.TargetSystemDescriptor; @@ -38,7 +39,7 @@ * @param the apply payload type * @param the rollback payload type */ -public abstract class AbstractTemplateLoadedChange extends AbstractLoadedChange { +public abstract class AbstractTemplateLoadedChange extends AbstractLoadedChange { private final List profiles; private final CONFIG configurationPayload; diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/MultiStepTemplateLoadedChange.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/MultiStepTemplateLoadedChange.java index 751a89ab8..2075e9e11 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/MultiStepTemplateLoadedChange.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/MultiStepTemplateLoadedChange.java @@ -16,6 +16,8 @@ package io.flamingock.internal.core.task.loaded; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplatePayload; +import io.flamingock.api.template.TemplatePayloadValidationError; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.common.core.task.RecoveryDescriptor; @@ -37,7 +39,7 @@ * @param the apply payload type * @param the rollback payload type */ -public class MultiStepTemplateLoadedChange +public class MultiStepTemplateLoadedChange extends AbstractTemplateLoadedChange { private final List> steps; @@ -75,10 +77,18 @@ protected List validateApplyPayload() { List errors = new ArrayList<>(); if (steps != null) { for (int i = 0; i < steps.size(); i++) { - if (steps.get(i).getApplyPayload() == null) { + APPLY applyPayload = steps.get(i).getApplyPayload(); + if (applyPayload == null) { errors.add(new ValidationError( String.format("Template '%s', step %d: missing required 'apply' payload", getSource(), i + 1), getId(), "change")); + } else { + List payloadErrors = applyPayload.validate(); + for (TemplatePayloadValidationError e : payloadErrors) { + errors.add(new ValidationError( + String.format("Template '%s', step %d apply payload: %s", getSource(), i + 1, e.getFormattedMessage()), + getId(), "change")); + } } } } @@ -88,12 +98,20 @@ protected List validateApplyPayload() { @Override protected List validateRollbackPayload() { List errors = new ArrayList<>(); - if (rollbackPayloadRequired && steps != null) { + if (steps != null) { for (int i = 0; i < steps.size(); i++) { - if (!steps.get(i).hasRollbackPayload()) { + ROLLBACK rollbackPayload = steps.get(i).getRollbackPayload(); + if (rollbackPayloadRequired && !steps.get(i).hasRollbackPayload()) { errors.add(new ValidationError( String.format("Template '%s', step %d: missing required 'rollback' payload (rollbackPayloadRequired=true)", getSource(), i + 1), getId(), "change")); + } else if (rollbackPayload != null) { + List payloadErrors = rollbackPayload.validate(); + for (TemplatePayloadValidationError e : payloadErrors) { + errors.add(new ValidationError( + String.format("Template '%s', step %d rollback payload: %s", getSource(), i + 1, e.getFormattedMessage()), + getId(), "change")); + } } } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java index d5bd8ba20..0b9d6047d 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java @@ -16,11 +16,14 @@ package io.flamingock.internal.core.task.loaded; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplatePayload; +import io.flamingock.api.template.TemplatePayloadValidationError; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.internal.common.core.task.TargetSystemDescriptor; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,7 +38,7 @@ * @param the apply payload type * @param the rollback payload type */ -public class SimpleTemplateLoadedChange +public class SimpleTemplateLoadedChange extends AbstractTemplateLoadedChange { // Already converted to typed payload (no longer raw Object from YAML) @@ -87,6 +90,16 @@ protected List validateApplyPayload() { String.format("Template '%s' requires 'apply' payload", getSource()), getId(), "change")); } + List payloadErrors = applyPayload.validate(); + if (!payloadErrors.isEmpty()) { + List errors = new ArrayList<>(); + for (TemplatePayloadValidationError e : payloadErrors) { + errors.add(new ValidationError( + String.format("Template '%s' apply payload: %s", getSource(), e.getFormattedMessage()), + getId(), "change")); + } + return errors; + } return Collections.emptyList(); } @@ -97,6 +110,18 @@ protected List validateRollbackPayload() { String.format("Template '%s' requires 'rollback' payload (rollbackPayloadRequired=true)", getSource()), getId(), "change")); } + if (rollbackPayload != null) { + List payloadErrors = rollbackPayload.validate(); + if (!payloadErrors.isEmpty()) { + List errors = new ArrayList<>(); + for (TemplatePayloadValidationError e : payloadErrors) { + errors.add(new ValidationError( + String.format("Template '%s' rollback payload: %s", getSource(), e.getFormattedMessage()), + getId(), "change")); + } + return errors; + } + } return Collections.emptyList(); } } 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 f823f9a02..4dc4f15c9 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 @@ -16,6 +16,7 @@ package io.flamingock.internal.core.task.loaded; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplatePayload; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.error.validation.ValidationResult; @@ -166,7 +167,7 @@ public TemplateLoadedTaskBuilder setSteps(Object steps) { } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) public AbstractTemplateLoadedChange build() { // boolean isTaskTransactional = true;//TODO implement this. isTaskTransactionalAccordingTemplate(templateSpec); ChangeTemplateDefinition definition = ChangeTemplateManager.getTemplate(templateName) @@ -184,32 +185,28 @@ public TemplateLoadedTaskBuilder setSteps(Object steps) { Constructor constructor = ReflectionUtil.getDefaultConstructor(definition.getTemplateClass()); // Determine template type from pre-resolved definition metadata. - // Note: Due to type erasure, we use Object bounds at construction time. + // Note: Due to type erasure, we use raw types at construction time. // The actual type safety comes from the conversion methods that use reflection // to determine the real types at runtime. boolean isMultiStep = definition.isMultiStep(); boolean rollbackPayloadRequired = definition.isRollbackPayloadRequired(); - if (isMultiStep) { - Class> steppableTemplateClass = - (Class>) - definition.getTemplateClass().asSubclass(AbstractChangeTemplate.class); + Class templateClass = + definition.getTemplateClass().asSubclass(AbstractChangeTemplate.class); - return getLoadedMultiStepTemplateChange(steppableTemplateClass, constructor, rollbackPayloadRequired); + if (isMultiStep) { + return getLoadedMultiStepTemplateChange(templateClass, constructor, rollbackPayloadRequired); } else { // Default to SimpleTemplateLoadedChange for simple templates (steppable=false or missing annotation) - Class> simpleTemplateClass = - (Class>) - definition.getTemplateClass().asSubclass(AbstractChangeTemplate.class); - - return getLoadedSimpleTemplateChange(simpleTemplateClass, constructor, rollbackPayloadRequired); + return getLoadedSimpleTemplateChange(templateClass, constructor, rollbackPayloadRequired); } } @NotNull - private MultiStepTemplateLoadedChange getLoadedMultiStepTemplateChange(Class> steppableTemplateClass, Constructor constructor, boolean rollbackPayloadRequired) { + @SuppressWarnings({"unchecked", "rawtypes"}) + private MultiStepTemplateLoadedChange getLoadedMultiStepTemplateChange(Class steppableTemplateClass, Constructor constructor, boolean rollbackPayloadRequired) { List> convertedSteps = convertSteps(constructor, steps);// Convert apply/rollback to typed payloads at load time - return new MultiStepTemplateLoadedChange<>( + return new MultiStepTemplateLoadedChange( fileName, id, order, @@ -228,9 +225,10 @@ private MultiStepTemplateLoadedChange getLoadedMultiStep } @NotNull - private SimpleTemplateLoadedChange getLoadedSimpleTemplateChange(Class> simpleTemplateClass, Constructor constructor, boolean rollbackPayloadRequired) { + @SuppressWarnings({"unchecked", "rawtypes"}) + private SimpleTemplateLoadedChange getLoadedSimpleTemplateChange(Class simpleTemplateClass, Constructor constructor, boolean rollbackPayloadRequired) { Pair convertedPayloads = convertPayloads(constructor, applyPayload, rollbackPayload);// Convert apply/rollback to typed payloads at load time - return new SimpleTemplateLoadedChange<>( + return new SimpleTemplateLoadedChange( fileName, id, order, @@ -242,8 +240,8 @@ private SimpleTemplateLoadedChange getLoadedSimpleTempla runAlways, system, configuration, - convertedPayloads.getFirst(), - convertedPayloads.getSecond(), + (TemplatePayload) convertedPayloads.getFirst(), + (TemplatePayload) convertedPayloads.getSecond(), targetSystem, recovery, rollbackPayloadRequired); diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTaskTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTaskTest.java index 57c119855..a57b6ad8c 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTaskTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTaskTest.java @@ -20,6 +20,7 @@ import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.TemplateStep; +import io.flamingock.api.template.wrappers.TemplateString; import io.flamingock.internal.common.core.error.ChangeExecutionException; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.recovery.action.ChangeAction; @@ -44,7 +45,7 @@ class SteppableTemplateExecutableTaskTest { private ExecutionRuntime mockRuntime; - private MultiStepTemplateLoadedChange mockDescriptor; + private MultiStepTemplateLoadedChange mockDescriptor; private Method applyMethod; private Method rollbackMethod; @@ -59,7 +60,7 @@ class SteppableTemplateExecutableTaskTest { * Test template that tracks apply and rollback invocations. */ @ChangeTemplate(name = "test-steppable-template", multiStep = true) - public static class TestSteppableTemplate extends AbstractChangeTemplate { + public static class TestSteppableTemplate extends AbstractChangeTemplate { public TestSteppableTemplate() { super(); @@ -70,7 +71,7 @@ public void apply() { if (shouldFailOnApply && appliedPayloads.size() == failAtStep) { throw new RuntimeException("Simulated apply failure at step " + failAtStep); } - appliedPayloads.add(applyPayload); + appliedPayloads.add(applyPayload.getValue()); } @Rollback @@ -78,7 +79,7 @@ public void rollback() { if (shouldFailOnRollback) { throw new RuntimeException("Simulated rollback failure"); } - rolledBackPayloads.add(rollbackPayload); + rolledBackPayloads.add(rollbackPayload.getValue()); } } @@ -86,7 +87,7 @@ public void rollback() { * Test template used to simulate null rollback method at executable level. */ @ChangeTemplate(name = "test-template-null-rollback-method", multiStep = true) - public static class TestTemplateWithNullRollbackMethod extends AbstractChangeTemplate { + public static class TestTemplateWithNullRollbackMethod extends AbstractChangeTemplate { public TestTemplateWithNullRollbackMethod() { super(); @@ -97,7 +98,7 @@ public void apply() { if (shouldFailOnApply && appliedPayloads.size() == failAtStep) { throw new RuntimeException("Simulated apply failure at step " + failAtStep); } - appliedPayloads.add(applyPayload); + appliedPayloads.add(applyPayload.getValue()); } @Rollback @@ -147,18 +148,22 @@ void setUp() throws NoSuchMethodException { }).when(mockRuntime).executeMethodWithInjectedDependencies(any(), any(Method.class)); } + private static TemplateString ts(String value) { + return new TemplateString(value); + } + @Test @DisplayName("Should apply all steps in sequence (step 0, 1, 2)") void shouldApplyAllStepsInOrder() { // Given - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-0"), - new TemplateStep<>("apply-1", "rollback-1"), - new TemplateStep<>("apply-2", "rollback-2") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-0")), + new TemplateStep<>(ts("apply-1"), ts("rollback-1")), + new TemplateStep<>(ts("apply-2"), ts("rollback-2")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -183,14 +188,14 @@ void shouldRollbackFromFailedStepInReverseOrder() { shouldFailOnApply = true; failAtStep = 2; // Fail at step index 2 (third step) - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-0"), - new TemplateStep<>("apply-1", "rollback-1"), - new TemplateStep<>("apply-2", "rollback-2") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-0")), + new TemplateStep<>(ts("apply-1"), ts("rollback-1")), + new TemplateStep<>(ts("apply-2"), ts("rollback-2")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -221,13 +226,13 @@ void shouldRollbackFromFailedStepInReverseOrder() { @DisplayName("Should set correct apply payload for each step during apply") void shouldSetCorrectPayloadDuringApply() { // Given - List> steps = Arrays.asList( - new TemplateStep<>("payload-A", "rollback-A"), - new TemplateStep<>("payload-B", "rollback-B") + List> steps = Arrays.asList( + new TemplateStep<>(ts("payload-A"), ts("rollback-A")), + new TemplateStep<>(ts("payload-B"), ts("rollback-B")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -251,14 +256,14 @@ void shouldSetCorrectPayloadDuringRollback() { shouldFailOnApply = true; failAtStep = 2; - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-payload-X"), - new TemplateStep<>("apply-1", "rollback-payload-Y"), - new TemplateStep<>("apply-2", "rollback-payload-Z") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-payload-X")), + new TemplateStep<>(ts("apply-1"), ts("rollback-payload-Y")), + new TemplateStep<>(ts("apply-2"), ts("rollback-payload-Z")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -285,15 +290,15 @@ void shouldSkipStepsWithoutRollbackPayload() { shouldFailOnApply = true; failAtStep = 3; // Fail at step index 3 (fourth step) - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-0"), - new TemplateStep<>("apply-1", null), // No rollback payload - should be skipped - new TemplateStep<>("apply-2", "rollback-2"), - new TemplateStep<>("apply-3", "rollback-3") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-0")), + new TemplateStep<>(ts("apply-1"), null), // No rollback payload - should be skipped + new TemplateStep<>(ts("apply-2"), ts("rollback-2")), + new TemplateStep<>(ts("apply-3"), ts("rollback-3")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -321,10 +326,10 @@ void shouldSkipStepsWithoutRollbackPayload() { @DisplayName("Should handle empty steps list without error") void shouldHandleEmptyStepsList() { // Given - List> emptySteps = Collections.emptyList(); + List> emptySteps = Collections.emptyList(); when(mockDescriptor.getSteps()).thenReturn(emptySteps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -355,13 +360,13 @@ void shouldNotRollbackWhenRollbackMethodIsNull() throws Exception { shouldFailOnApply = true; failAtStep = 1; - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-0"), - new TemplateStep<>("apply-1", "rollback-1") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-0")), + new TemplateStep<>(ts("apply-1"), ts("rollback-1")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -384,13 +389,13 @@ void shouldThrowChangeExecutionExceptionOnApplyFailure() { shouldFailOnApply = true; failAtStep = 1; - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-0"), - new TemplateStep<>("apply-1", "rollback-1") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-0")), + new TemplateStep<>(ts("apply-1"), ts("rollback-1")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -416,13 +421,13 @@ void shouldThrowChangeExecutionExceptionOnRollbackFailure() { failAtStep = 1; shouldFailOnRollback = true; - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-0"), - new TemplateStep<>("apply-1", "rollback-1") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-0")), + new TemplateStep<>(ts("apply-1"), ts("rollback-1")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, @@ -450,15 +455,15 @@ void shouldMaintainStepIndexAcrossApplyAndRollback() { shouldFailOnApply = true; failAtStep = 2; - List> steps = Arrays.asList( - new TemplateStep<>("apply-0", "rollback-0"), - new TemplateStep<>("apply-1", "rollback-1"), - new TemplateStep<>("apply-2", "rollback-2"), - new TemplateStep<>("apply-3", "rollback-3") + List> steps = Arrays.asList( + new TemplateStep<>(ts("apply-0"), ts("rollback-0")), + new TemplateStep<>(ts("apply-1"), ts("rollback-1")), + new TemplateStep<>(ts("apply-2"), ts("rollback-2")), + new TemplateStep<>(ts("apply-3"), ts("rollback-3")) ); when(mockDescriptor.getSteps()).thenReturn(steps); - SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( "test-stage", mockDescriptor, ChangeAction.APPLY, diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java index 963c379e9..652cf4398 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java @@ -22,6 +22,7 @@ import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.wrappers.TemplateString; import io.flamingock.api.annotations.Apply; import io.flamingock.internal.core.pipeline.loaded.stage.StageValidationContext; import org.junit.jupiter.api.BeforeEach; @@ -43,7 +44,7 @@ class SimpleTemplateLoadedTaskBuilderTest { // Simple test template implementation using the annotation @ChangeTemplate(name = "test-change-template") - public static class TestChangeTemplate extends AbstractChangeTemplate { + public static class TestChangeTemplate extends AbstractChangeTemplate { public TestChangeTemplate() { super(); 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 f0c2f52e2..b489d9de8 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,6 +20,7 @@ import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.TemplateStep; +import io.flamingock.api.template.wrappers.TemplateString; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.common.core.preview.TemplatePreviewChange; @@ -49,7 +50,7 @@ class SteppableTemplateLoadedTaskBuilderTest { // Steppable test template implementation using the annotation @ChangeTemplate(name = "test-steppable-template", multiStep = true) - public static class TestSteppableTemplate extends AbstractChangeTemplate { + public static class TestSteppableTemplate extends AbstractChangeTemplate { public TestSteppableTemplate() { super(); @@ -67,7 +68,7 @@ public void rollback() { // Simple test template implementation @ChangeTemplate(name = "test-simple-template") - public static class TestSimpleTemplate extends AbstractChangeTemplate { + public static class TestSimpleTemplate extends AbstractChangeTemplate { public TestSimpleTemplate() { super(); @@ -258,10 +259,10 @@ void shouldBuildWithMultipleSteps() { List> steps = steppableResult.getSteps(); assertNotNull(steps); assertEquals(3, steps.size()); - // Verify steps are preserved in order - payloads are now typed objects - assertEquals("createCollection", steps.get(0).getApplyPayload()); - assertEquals("insertDocument", steps.get(1).getApplyPayload()); - assertEquals("createIndex", steps.get(2).getApplyPayload()); + // Verify steps are preserved in order - payloads are now typed TemplateString objects + assertEquals(new TemplateString("createCollection"), steps.get(0).getApplyPayload()); + assertEquals(new TemplateString("insertDocument"), steps.get(1).getApplyPayload()); + assertEquals(new TemplateString("createIndex"), steps.get(2).getApplyPayload()); } } 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 1c033d2fd..9c7fb79e6 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 @@ -20,6 +20,7 @@ import io.flamingock.internal.common.core.template.ChangeTemplateFileContent; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.wrappers.TemplateString; 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; @@ -128,7 +129,7 @@ private AbstractLoadedTask getTemplateLoadedChange(String profiles) { } @ChangeTemplate(name = "template-simulate") - public static class TemplateSimulate extends AbstractChangeTemplate { + public static class TemplateSimulate extends AbstractChangeTemplate { public TemplateSimulate() { super(); } diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/FileUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/FileUtil.java index 63e2d5841..f05502c41 100644 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/FileUtil.java +++ b/utils/general-util/src/main/java/io/flamingock/internal/util/FileUtil.java @@ -78,6 +78,21 @@ public static T getFromYamlFile(File file, Class type) { } public static T getFromMap(Class type, Object source) { + // Direct match shortcut + if (type.isInstance(source)) { + return type.cast(source); + } + // Scalar-to-constructor conversion: if source is a scalar type and target has a matching constructor + if (source instanceof String || source instanceof Number || source instanceof Boolean) { + try { + java.lang.reflect.Constructor ctor = type.getConstructor(source.getClass()); + return ctor.newInstance(source); + } catch (NoSuchMethodException ignored) { + // Fall through to YAML round-trip + } catch (Exception e) { + throw new RuntimeException("Failed to construct " + type.getSimpleName() + " from scalar: " + e.getMessage(), e); + } + } Yaml yamlWriter = new Yaml(); StringWriter writer = new StringWriter(); yamlWriter.dump(source, writer);