Skip to content

Commit 0bee4e9

Browse files
authored
refactor: check basic template format at compilation time (#837)
1 parent 69b56b0 commit 0bee4e9

8 files changed

Lines changed: 902 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,40 @@ When a change fails and cannot be rolled back:
396396

397397
**File**: `core/flamingock-core-api/src/main/java/io/flamingock/api/RecoveryStrategy.java`
398398

399+
### Compile-Time Template Validation
400+
401+
Templates are validated at compile-time to ensure YAML structure matches the template type:
402+
403+
**SimpleTemplate** (`AbstractSimpleTemplate`):
404+
- MUST have `apply` field
405+
- MAY have `rollback` field
406+
- MUST NOT have `steps` field
407+
408+
**SteppableTemplate** (`AbstractSteppableTemplate`):
409+
- MUST have `steps` field
410+
- MUST NOT have `apply` or `rollback` fields at root level
411+
- Each step MUST have `apply` field
412+
413+
**Configuration:**
414+
```java
415+
@EnableFlamingock(
416+
configFile = "pipeline.yaml",
417+
strictTemplateValidation = true // default
418+
)
419+
```
420+
421+
| Flag Value | Behavior |
422+
|------------|----------|
423+
| `true` (default) | Compilation fails with detailed error |
424+
| `false` | Warning logged, compilation continues |
425+
426+
**Validation Location:** `TemplateValidator` in `core/flamingock-core-commons/.../template/`
427+
428+
**Key Files:**
429+
- `io.flamingock.internal.common.core.template.TemplateValidator` - validation logic
430+
- `io.flamingock.api.annotations.EnableFlamingock` - strictTemplateValidation flag
431+
- `io.flamingock.api.template.AbstractChangeTemplate` - template base classes
432+
399433
### Dependency Injection in Templates
400434

401435
Template methods (`@Apply`, `@Rollback`) receive dependencies as **method parameters**, not constructor injection:

core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,27 @@
127127
* When false, only a warning is emitted.
128128
*/
129129
boolean strictStageMapping() default true;
130+
131+
/**
132+
* If true, the annotation processor validates that all template-based changes
133+
* have YAML structure matching their template type (Simple vs Steppable).
134+
* <p>
135+
* <strong>SimpleTemplate</strong> validation:
136+
* <ul>
137+
* <li>MUST have {@code apply} field</li>
138+
* <li>MAY have {@code rollback} field</li>
139+
* <li>MUST NOT have {@code steps} field</li>
140+
* </ul>
141+
* <p>
142+
* <strong>SteppableTemplate</strong> validation:
143+
* <ul>
144+
* <li>MUST have {@code steps} field</li>
145+
* <li>MUST NOT have {@code apply} or {@code rollback} fields at root level</li>
146+
* <li>Each step MUST have {@code apply} field</li>
147+
* </ul>
148+
* <p>
149+
* When validation fails and this flag is {@code true} (default), a RuntimeException
150+
* is thrown at compilation time. When {@code false}, only a warning is emitted.
151+
*/
152+
boolean strictTemplateValidation() default true;
130153
}

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ public abstract class AbstractChangeTemplate<SHARED_CONFIGURATION_FIELD, APPLY_F
4444
protected boolean isTransactional;
4545
protected SHARED_CONFIGURATION_FIELD configuration;
4646

47-
private final Set<Class<?>> reflectiveClasses;
47+
private final Set<Class<?>> additionalReflectiveClasses;
4848

4949

5050
@SuppressWarnings("unchecked")
5151
public AbstractChangeTemplate(Class<?>... additionalReflectiveClass) {
52-
reflectiveClasses = new HashSet<>(Arrays.asList(additionalReflectiveClass));
52+
// Store additional classes - reflective classes set is built on-demand in getReflectiveClasses()
53+
this.additionalReflectiveClasses = new HashSet<>(Arrays.asList(additionalReflectiveClass));
5354

5455
try {
5556
Class<?>[] typeArgs = ReflectionUtil.resolveTypeArgumentsAsClasses(this.getClass(), AbstractChangeTemplate.class);
@@ -61,20 +62,37 @@ public AbstractChangeTemplate(Class<?>... additionalReflectiveClass) {
6162
this.configurationClass = (Class<SHARED_CONFIGURATION_FIELD>) typeArgs[0];
6263
this.applyPayloadClass = (Class<APPLY_FIELD>) typeArgs[1];
6364
this.rollbackPayloadClass = (Class<ROLLBACK_FIELD>) typeArgs[2];
64-
65-
reflectiveClasses.add(configurationClass);
66-
reflectiveClasses.add(applyPayloadClass);
67-
reflectiveClasses.add(rollbackPayloadClass);
68-
reflectiveClasses.add(TemplateStep.class);
6965
} catch (ClassCastException e) {
7066
throw new IllegalStateException("Generic type arguments for a Template must be concrete types (classes, interfaces, or primitive wrappers like String, Integer, etc.): " + e.getMessage(), e);
7167
} catch (Exception e) {
7268
throw new IllegalStateException("Failed to initialize template: " + e.getMessage(), e);
7369
}
7470
}
7571

72+
/**
73+
* Returns the collection of classes that need reflection registration for GraalVM native images.
74+
* <p>
75+
* This method builds the reflective classes set on-demand, including:
76+
* <ul>
77+
* <li>The configuration class (generic type argument 0)</li>
78+
* <li>The apply payload class (generic type argument 1)</li>
79+
* <li>The rollback payload class (generic type argument 2)</li>
80+
* <li>{@link TemplateStep} class</li>
81+
* <li>Any additional classes passed to the constructor</li>
82+
* </ul>
83+
* <p>
84+
* This method is only called by GraalVM's {@code RegistrationFeature} at build-time,
85+
* so there is no performance concern from building the set on each call.
86+
*
87+
* @return collection of classes requiring reflection registration
88+
*/
7689
@Override
7790
public final Collection<Class<?>> getReflectiveClasses() {
91+
Set<Class<?>> reflectiveClasses = new HashSet<>(additionalReflectiveClasses);
92+
reflectiveClasses.add(configurationClass);
93+
reflectiveClasses.add(applyPayloadClass);
94+
reflectiveClasses.add(rollbackPayloadClass);
95+
reflectiveClasses.add(TemplateStep.class);
7896
return reflectiveClasses;
7997
}
8098

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 io.flamingock.api.annotations.Apply;
19+
import org.junit.jupiter.api.DisplayName;
20+
import org.junit.jupiter.api.Test;
21+
22+
import java.util.Collection;
23+
24+
import static org.junit.jupiter.api.Assertions.*;
25+
26+
class AbstractChangeTemplateReflectiveClassesTest {
27+
28+
// Simple test configuration class
29+
public static class TestConfig {
30+
public String configValue;
31+
}
32+
33+
// Simple test apply payload class
34+
public static class TestApplyPayload {
35+
public String applyData;
36+
}
37+
38+
// Simple test rollback payload class
39+
public static class TestRollbackPayload {
40+
public String rollbackData;
41+
}
42+
43+
// Additional class for reflection
44+
public static class AdditionalClass {
45+
public String additionalData;
46+
}
47+
48+
// Another additional class for reflection
49+
public static class AnotherAdditionalClass {
50+
public String moreData;
51+
}
52+
53+
// Test template with custom generic types
54+
public static class TestTemplateWithCustomTypes
55+
extends AbstractSimpleTemplate<TestConfig, TestApplyPayload, TestRollbackPayload> {
56+
57+
public TestTemplateWithCustomTypes() {
58+
super();
59+
}
60+
61+
@Apply
62+
public void apply() {
63+
// Test implementation
64+
}
65+
}
66+
67+
// Test template with additional reflective classes
68+
public static class TestTemplateWithAdditionalClasses
69+
extends AbstractSimpleTemplate<TestConfig, TestApplyPayload, TestRollbackPayload> {
70+
71+
public TestTemplateWithAdditionalClasses() {
72+
super(AdditionalClass.class, AnotherAdditionalClass.class);
73+
}
74+
75+
@Apply
76+
public void apply() {
77+
// Test implementation
78+
}
79+
}
80+
81+
// Test template with Void configuration
82+
public static class TestTemplateWithVoidConfig
83+
extends AbstractSimpleTemplate<Void, String, String> {
84+
85+
public TestTemplateWithVoidConfig() {
86+
super();
87+
}
88+
89+
@Apply
90+
public void apply() {
91+
// Test implementation
92+
}
93+
}
94+
95+
@Test
96+
@DisplayName("getReflectiveClasses should return set containing configuration class")
97+
void getReflectiveClassesShouldContainConfigurationClass() {
98+
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
99+
100+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
101+
102+
assertTrue(reflectiveClasses.contains(TestConfig.class),
103+
"Should contain configuration class TestConfig");
104+
}
105+
106+
@Test
107+
@DisplayName("getReflectiveClasses should return set containing apply payload class")
108+
void getReflectiveClassesShouldContainApplyPayloadClass() {
109+
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
110+
111+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
112+
113+
assertTrue(reflectiveClasses.contains(TestApplyPayload.class),
114+
"Should contain apply payload class TestApplyPayload");
115+
}
116+
117+
@Test
118+
@DisplayName("getReflectiveClasses should return set containing rollback payload class")
119+
void getReflectiveClassesShouldContainRollbackPayloadClass() {
120+
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
121+
122+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
123+
124+
assertTrue(reflectiveClasses.contains(TestRollbackPayload.class),
125+
"Should contain rollback payload class TestRollbackPayload");
126+
}
127+
128+
@Test
129+
@DisplayName("getReflectiveClasses should return set containing TemplateStep class")
130+
void getReflectiveClassesShouldContainTemplateStepClass() {
131+
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
132+
133+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
134+
135+
assertTrue(reflectiveClasses.contains(TemplateStep.class),
136+
"Should contain TemplateStep class");
137+
}
138+
139+
@Test
140+
@DisplayName("getReflectiveClasses should include additional reflective classes passed to constructor")
141+
void getReflectiveClassesShouldIncludeAdditionalClasses() {
142+
TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses();
143+
144+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
145+
146+
assertTrue(reflectiveClasses.contains(AdditionalClass.class),
147+
"Should contain AdditionalClass");
148+
assertTrue(reflectiveClasses.contains(AnotherAdditionalClass.class),
149+
"Should contain AnotherAdditionalClass");
150+
}
151+
152+
@Test
153+
@DisplayName("Multiple calls to getReflectiveClasses should return equivalent sets")
154+
void multipleCallsShouldReturnEquivalentSets() {
155+
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
156+
157+
Collection<Class<?>> firstCall = template.getReflectiveClasses();
158+
Collection<Class<?>> secondCall = template.getReflectiveClasses();
159+
160+
assertEquals(firstCall.size(), secondCall.size(),
161+
"Both calls should return sets of the same size");
162+
assertTrue(firstCall.containsAll(secondCall),
163+
"First call should contain all elements of second call");
164+
assertTrue(secondCall.containsAll(firstCall),
165+
"Second call should contain all elements of first call");
166+
}
167+
168+
@Test
169+
@DisplayName("getReflectiveClasses with Void configuration should include Void class")
170+
void getReflectiveClassesWithVoidConfigShouldIncludeVoidClass() {
171+
TestTemplateWithVoidConfig template = new TestTemplateWithVoidConfig();
172+
173+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
174+
175+
assertTrue(reflectiveClasses.contains(Void.class),
176+
"Should contain Void class for configuration");
177+
assertTrue(reflectiveClasses.contains(String.class),
178+
"Should contain String class for apply/rollback payloads");
179+
}
180+
181+
@Test
182+
@DisplayName("getReflectiveClasses should return at least 4 classes (config, apply, rollback, TemplateStep)")
183+
void getReflectiveClassesShouldReturnAtLeast4Classes() {
184+
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();
185+
186+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
187+
188+
assertTrue(reflectiveClasses.size() >= 4,
189+
"Should return at least 4 classes (config, apply, rollback, TemplateStep)");
190+
}
191+
192+
@Test
193+
@DisplayName("getReflectiveClasses with additional classes should return more than 4 classes")
194+
void getReflectiveClassesWithAdditionalClassesShouldReturnMoreThan4() {
195+
TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses();
196+
197+
Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();
198+
199+
assertTrue(reflectiveClasses.size() >= 6,
200+
"Should return at least 6 classes (config, apply, rollback, TemplateStep, + 2 additional)");
201+
}
202+
}

0 commit comments

Comments
 (0)