Skip to content

Commit 8cb45bd

Browse files
dieppaclaude
andauthored
feat(templates): add TemplatePayloadInfo and restrict tx validation to apply payloads (#857)
- Add **TemplatePayloadInfo** class with `supportsTransactions` field so payloads can declare transaction compatibility via `TemplatePayload.getInfo()` - Implement `getInfo()` in **TemplateString**, **TemplateVoid**, and test payload stubs - Add `checkPayloadTransactionSupport()` and `getExplicitTransactionSupport()` helpers to **AbstractTemplateLoadedChange** for centralized transaction-support checking - Restrict transaction-support validation to **apply payloads only** — configuration is shared state and rollback is a compensating action, neither represents an actual operation against a target system - `warnIfAllPayloadsSupportTransactions()` now inspects only apply payloads in both **SimpleTemplateLoadedChange** and **MultiStepTemplateLoadedChange** - Add **PayloadTransactionSupportValidationTest** with 10 cases covering error, warning, and not-checked paths for both simple and multi-step templates Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 44aeeb4 commit 8cb45bd

9 files changed

Lines changed: 462 additions & 18 deletions

File tree

core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplatePayload.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,12 @@ public interface TemplatePayload {
3333
* @return list of validation errors, empty if payload is valid
3434
*/
3535
List<TemplatePayloadValidationError> validate(TemplateValidationContext context);
36+
37+
/**
38+
* Returns metadata about this payload so the framework can make
39+
* centralized decisions based on payload characteristics.
40+
*
41+
* @return payload info; never {@code null}
42+
*/
43+
TemplatePayloadInfo getInfo();
3644
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.api.template;
17+
18+
import java.util.Optional;
19+
20+
/**
21+
* Metadata that a {@link TemplatePayload} exposes to the framework so
22+
* centralized decisions can be made based on payload characteristics.
23+
*
24+
* <p><b>Binary-compatibility contract:</b> new fields are added as
25+
* getter/setter pairs. A {@code null} value means "not specified" —
26+
* the payload makes no claim and the framework applies its own policy.
27+
* Older implementations that return a default-constructed instance
28+
* continue to work unchanged as new fields are introduced.
29+
*
30+
* <p>Current fields:
31+
* <ul>
32+
* <li>{@code supportsTransactions} — whether the payload's target
33+
* system supports transactional execution. {@code null} (default)
34+
* means the payload makes no claim.</li>
35+
* </ul>
36+
*/
37+
public class TemplatePayloadInfo {
38+
39+
private Boolean supportsTransactions;
40+
41+
/**
42+
* Creates an info instance with all fields set to {@code null}
43+
* (no claims made).
44+
*/
45+
public TemplatePayloadInfo() {
46+
}
47+
48+
/**
49+
* Returns whether the payload's target system supports transactional
50+
* execution.
51+
*
52+
* @return an {@link Optional} containing {@code true} or {@code false}
53+
* if the payload explicitly declares support; empty if the
54+
* payload makes no claim
55+
*/
56+
public Optional<Boolean> getSupportsTransactions() {
57+
return Optional.ofNullable(supportsTransactions);
58+
}
59+
60+
/**
61+
* Sets whether the payload's target system supports transactional
62+
* execution.
63+
*
64+
* @param supportsTransactions {@code true} if transactions are supported,
65+
* {@code false} if not, or {@code null} to
66+
* make no claim
67+
*/
68+
public void setSupportsTransactions(Boolean supportsTransactions) {
69+
this.supportsTransactions = supportsTransactions;
70+
}
71+
}

core/flamingock-core-api/src/main/java/io/flamingock/api/template/wrappers/TemplateString.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.flamingock.api.template.wrappers;
1717

1818
import io.flamingock.api.template.TemplatePayload;
19+
import io.flamingock.api.template.TemplatePayloadInfo;
1920
import io.flamingock.api.template.TemplatePayloadValidationError;
2021
import io.flamingock.api.template.TemplateValidationContext;
2122

@@ -67,6 +68,11 @@ public List<TemplatePayloadValidationError> validate(TemplateValidationContext c
6768
return Collections.emptyList();
6869
}
6970

71+
@Override
72+
public TemplatePayloadInfo getInfo() {
73+
return new TemplatePayloadInfo();
74+
}
75+
7076
@Override
7177
public String toString() {
7278
return value;

core/flamingock-core-api/src/main/java/io/flamingock/api/template/wrappers/TemplateVoid.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.flamingock.api.template.wrappers;
1717

1818
import io.flamingock.api.template.TemplatePayload;
19+
import io.flamingock.api.template.TemplatePayloadInfo;
1920
import io.flamingock.api.template.TemplatePayloadValidationError;
2021
import io.flamingock.api.template.TemplateValidationContext;
2122

@@ -36,4 +37,9 @@ public class TemplateVoid implements TemplatePayload {
3637
public List<TemplatePayloadValidationError> validate(TemplateValidationContext context) {
3738
return Collections.emptyList();
3839
}
40+
41+
@Override
42+
public TemplatePayloadInfo getInfo() {
43+
return new TemplatePayloadInfo();
44+
}
3945
}

core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public static class TestConfig implements TemplatePayload {
3939
public List<TemplatePayloadValidationError> validate(TemplateValidationContext context) {
4040
return Collections.emptyList();
4141
}
42+
43+
@Override
44+
public TemplatePayloadInfo getInfo() {
45+
return new TemplatePayloadInfo();
46+
}
4247
}
4348

4449
// Simple test apply payload class
@@ -49,6 +54,11 @@ public static class TestApplyPayload implements TemplatePayload {
4954
public List<TemplatePayloadValidationError> validate(TemplateValidationContext context) {
5055
return Collections.emptyList();
5156
}
57+
58+
@Override
59+
public TemplatePayloadInfo getInfo() {
60+
return new TemplatePayloadInfo();
61+
}
5262
}
5363

5464
// Simple test rollback payload class
@@ -59,6 +69,11 @@ public static class TestRollbackPayload implements TemplatePayload {
5969
public List<TemplatePayloadValidationError> validate(TemplateValidationContext context) {
6070
return Collections.emptyList();
6171
}
72+
73+
@Override
74+
public TemplatePayloadInfo getInfo() {
75+
return new TemplatePayloadInfo();
76+
}
6277
}
6378

6479
// Additional class for reflection

core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractTemplateLoadedChange.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@
2525
import io.flamingock.internal.common.core.task.TargetSystemDescriptor;
2626
import io.flamingock.internal.core.pipeline.loaded.stage.StageValidationContext;
2727
import io.flamingock.internal.util.ReflectionUtil;
28+
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
29+
import org.slf4j.Logger;
2830

2931
import java.lang.reflect.Constructor;
3032
import java.lang.reflect.Method;
33+
import java.util.Collections;
3134
import java.util.List;
3235
import java.util.Optional;
3336

@@ -42,6 +45,8 @@
4245
*/
4346
public abstract class AbstractTemplateLoadedChange<CONFIG extends TemplatePayload, APPLY extends TemplatePayload, ROLLBACK extends TemplatePayload> extends AbstractLoadedChange {
4447

48+
protected static final Logger logger = FlamingockLoggerFactory.getLogger(AbstractTemplateLoadedChange.class);
49+
4550
private final List<String> profiles;
4651
private final CONFIG configurationPayload;
4752
protected final boolean rollbackPayloadRequired;
@@ -101,6 +106,7 @@ public List<ValidationError> getValidationErrors(StageValidationContext context)
101106
errors.addAll(validateConfigurationPayload());
102107
errors.addAll(validateApplyPayload());
103108
errors.addAll(validateRollbackPayload());
109+
warnIfAllPayloadsSupportTransactions();
104110
return errors;
105111
}
106112

@@ -110,9 +116,32 @@ protected TemplateValidationContext buildValidationContext() {
110116
return ctx;
111117
}
112118

119+
protected List<ValidationError> checkPayloadTransactionSupport(TemplatePayload payload, String payloadLabel) {
120+
if (payload == null) {
121+
return Collections.emptyList();
122+
}
123+
Optional<Boolean> supports = payload.getInfo().getSupportsTransactions();
124+
if (isTransactional() && supports.isPresent() && !supports.get()) {
125+
return Collections.singletonList(new ValidationError(
126+
String.format("Template '%s' is transactional but %s payload does not support transactions",
127+
getSource(), payloadLabel),
128+
getId(), "change"));
129+
}
130+
return Collections.emptyList();
131+
}
132+
133+
protected static Boolean getExplicitTransactionSupport(TemplatePayload payload) {
134+
if (payload == null) {
135+
return null;
136+
}
137+
return payload.getInfo().getSupportsTransactions().orElse(null);
138+
}
139+
113140
abstract protected List<ValidationError> validateConfigurationPayload();
114141

115142
abstract protected List<ValidationError> validateApplyPayload();
116143

117144
abstract protected List<ValidationError> validateRollbackPayload();
145+
146+
abstract protected void warnIfAllPayloadsSupportTransactions();
118147
}

core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/MultiStepTemplateLoadedChange.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ protected List<ValidationError> validateApplyPayload() {
105105
String.format("Template '%s', step %d apply payload: %s", getSource(), i + 1, e.getFormattedMessage()),
106106
getId(), "change"));
107107
}
108+
errors.addAll(checkPayloadTransactionSupport(applyPayload, "step " + (i + 1) + " apply"));
108109
}
109110
}
110111
}
@@ -134,4 +135,29 @@ protected List<ValidationError> validateRollbackPayload() {
134135
}
135136
return errors;
136137
}
138+
139+
@Override
140+
protected void warnIfAllPayloadsSupportTransactions() {
141+
if (isTransactional()) {
142+
return;
143+
}
144+
if (steps == null || steps.isEmpty()) {
145+
return;
146+
}
147+
boolean atLeastOneExplicit = false;
148+
for (TemplateStep<APPLY, ROLLBACK> step : steps) {
149+
Boolean supports = getExplicitTransactionSupport(step.getApplyPayload());
150+
if (supports == null) {
151+
continue;
152+
}
153+
if (!supports) {
154+
return;
155+
}
156+
atLeastOneExplicit = true;
157+
}
158+
if (atLeastOneExplicit) {
159+
logger.warn("Template '{}': all apply payloads support transactions but change is not transactional. " +
160+
"Consider setting transactional=true", getSource());
161+
}
162+
}
137163
}

core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,21 @@ protected List<ValidationError> validateApplyPayload() {
105105
String.format("Template '%s' requires 'apply' payload", getSource()),
106106
getId(), "change"));
107107
}
108+
List<ValidationError> errors = new ArrayList<>();
108109
TemplateValidationContext context = buildValidationContext();
109110
List<TemplatePayloadValidationError> payloadErrors = applyPayload.validate(context);
110-
if (!payloadErrors.isEmpty()) {
111-
List<ValidationError> errors = new ArrayList<>();
112-
for (TemplatePayloadValidationError e : payloadErrors) {
113-
errors.add(new ValidationError(
114-
String.format("Template '%s' apply payload: %s", getSource(), e.getFormattedMessage()),
115-
getId(), "change"));
116-
}
117-
return errors;
111+
for (TemplatePayloadValidationError e : payloadErrors) {
112+
errors.add(new ValidationError(
113+
String.format("Template '%s' apply payload: %s", getSource(), e.getFormattedMessage()),
114+
getId(), "change"));
118115
}
119-
return Collections.emptyList();
116+
errors.addAll(checkPayloadTransactionSupport(applyPayload, "apply"));
117+
return errors;
120118
}
121119

122120
@Override
123121
protected List<ValidationError> validateRollbackPayload() {
122+
List<ValidationError> errors = new ArrayList<>();
124123
if (rollbackPayloadRequired && rollbackPayload == null) {
125124
return Collections.singletonList(new ValidationError(
126125
String.format("Template '%s' requires 'rollback' payload (rollbackPayloadRequired=true)", getSource()),
@@ -129,16 +128,24 @@ protected List<ValidationError> validateRollbackPayload() {
129128
if (rollbackPayload != null) {
130129
TemplateValidationContext context = buildValidationContext();
131130
List<TemplatePayloadValidationError> payloadErrors = rollbackPayload.validate(context);
132-
if (!payloadErrors.isEmpty()) {
133-
List<ValidationError> errors = new ArrayList<>();
134-
for (TemplatePayloadValidationError e : payloadErrors) {
135-
errors.add(new ValidationError(
136-
String.format("Template '%s' rollback payload: %s", getSource(), e.getFormattedMessage()),
137-
getId(), "change"));
138-
}
139-
return errors;
131+
for (TemplatePayloadValidationError e : payloadErrors) {
132+
errors.add(new ValidationError(
133+
String.format("Template '%s' rollback payload: %s", getSource(), e.getFormattedMessage()),
134+
getId(), "change"));
140135
}
141136
}
142-
return Collections.emptyList();
137+
return errors;
138+
}
139+
140+
@Override
141+
protected void warnIfAllPayloadsSupportTransactions() {
142+
if (isTransactional()) {
143+
return;
144+
}
145+
Boolean supports = getExplicitTransactionSupport(applyPayload);
146+
if (Boolean.TRUE.equals(supports)) {
147+
logger.warn("Template '{}': apply payload supports transactions but change is not transactional. " +
148+
"Consider setting transactional=true", getSource());
149+
}
143150
}
144151
}

0 commit comments

Comments
 (0)