Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,40 @@ When a change fails and cannot be rolled back:

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

### Compile-Time Template Validation

Templates are validated at compile-time to ensure YAML structure matches the template type:

**SimpleTemplate** (`AbstractSimpleTemplate`):
- MUST have `apply` field
- MAY have `rollback` field
- MUST NOT have `steps` field

**SteppableTemplate** (`AbstractSteppableTemplate`):
- MUST have `steps` field
- MUST NOT have `apply` or `rollback` fields at root level
- Each step MUST have `apply` field

**Configuration:**
```java
@EnableFlamingock(
configFile = "pipeline.yaml",
strictTemplateValidation = true // default
)
```

| Flag Value | Behavior |
|------------|----------|
| `true` (default) | Compilation fails with detailed error |
| `false` | Warning logged, compilation continues |

**Validation Location:** `TemplateValidator` in `core/flamingock-core-commons/.../template/`

**Key Files:**
- `io.flamingock.internal.common.core.template.TemplateValidator` - validation logic
- `io.flamingock.api.annotations.EnableFlamingock` - strictTemplateValidation flag
- `io.flamingock.api.template.AbstractChangeTemplate` - template base classes

### Dependency Injection in Templates

Template methods (`@Apply`, `@Rollback`) receive dependencies as **method parameters**, not constructor injection:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,27 @@
* When false, only a warning is emitted.
*/
boolean strictStageMapping() default true;

/**
* If true, the annotation processor validates that all template-based changes
* have YAML structure matching their template type (Simple vs Steppable).
* <p>
* <strong>SimpleTemplate</strong> validation:
* <ul>
* <li>MUST have {@code apply} field</li>
* <li>MAY have {@code rollback} field</li>
* <li>MUST NOT have {@code steps} field</li>
* </ul>
* <p>
* <strong>SteppableTemplate</strong> validation:
* <ul>
* <li>MUST have {@code steps} field</li>
* <li>MUST NOT have {@code apply} or {@code rollback} fields at root level</li>
* <li>Each step MUST have {@code apply} field</li>
* </ul>
* <p>
* When validation fails and this flag is {@code true} (default), a RuntimeException
* is thrown at compilation time. When {@code false}, only a warning is emitted.
*/
boolean strictTemplateValidation() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ public abstract class AbstractChangeTemplate<SHARED_CONFIGURATION_FIELD, APPLY_F
protected boolean isTransactional;
protected SHARED_CONFIGURATION_FIELD configuration;

private final Set<Class<?>> reflectiveClasses;
private final Set<Class<?>> additionalReflectiveClasses;


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

try {
Class<?>[] typeArgs = ReflectionUtil.resolveTypeArgumentsAsClasses(this.getClass(), AbstractChangeTemplate.class);
Expand All @@ -61,20 +62,37 @@ public AbstractChangeTemplate(Class<?>... additionalReflectiveClass) {
this.configurationClass = (Class<SHARED_CONFIGURATION_FIELD>) typeArgs[0];
this.applyPayloadClass = (Class<APPLY_FIELD>) typeArgs[1];
this.rollbackPayloadClass = (Class<ROLLBACK_FIELD>) typeArgs[2];

reflectiveClasses.add(configurationClass);
reflectiveClasses.add(applyPayloadClass);
reflectiveClasses.add(rollbackPayloadClass);
reflectiveClasses.add(TemplateStep.class);
} catch (ClassCastException e) {
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);
} catch (Exception e) {
throw new IllegalStateException("Failed to initialize template: " + e.getMessage(), e);
}
}

/**
* Returns the collection of classes that need reflection registration for GraalVM native images.
* <p>
* This method builds the reflective classes set on-demand, including:
* <ul>
* <li>The configuration class (generic type argument 0)</li>
* <li>The apply payload class (generic type argument 1)</li>
* <li>The rollback payload class (generic type argument 2)</li>
* <li>{@link TemplateStep} class</li>
* <li>Any additional classes passed to the constructor</li>
* </ul>
* <p>
* This method is only called by GraalVM's {@code RegistrationFeature} at build-time,
* so there is no performance concern from building the set on each call.
*
* @return collection of classes requiring reflection registration
*/
@Override
public final Collection<Class<?>> getReflectiveClasses() {
Set<Class<?>> reflectiveClasses = new HashSet<>(additionalReflectiveClasses);
reflectiveClasses.add(configurationClass);
reflectiveClasses.add(applyPayloadClass);
reflectiveClasses.add(rollbackPayloadClass);
reflectiveClasses.add(TemplateStep.class);
return reflectiveClasses;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* 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 io.flamingock.api.annotations.Apply;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Collection;

import static org.junit.jupiter.api.Assertions.*;

class AbstractChangeTemplateReflectiveClassesTest {

// Simple test configuration class
public static class TestConfig {
public String configValue;
}

// Simple test apply payload class
public static class TestApplyPayload {
public String applyData;
}

// Simple test rollback payload class
public static class TestRollbackPayload {
public String rollbackData;
}

// Additional class for reflection
public static class AdditionalClass {
public String additionalData;
}

// Another additional class for reflection
public static class AnotherAdditionalClass {
public String moreData;
}

// Test template with custom generic types
public static class TestTemplateWithCustomTypes
extends AbstractSimpleTemplate<TestConfig, TestApplyPayload, TestRollbackPayload> {

public TestTemplateWithCustomTypes() {
super();
}

@Apply
public void apply() {
// Test implementation
}
}

// Test template with additional reflective classes
public static class TestTemplateWithAdditionalClasses
extends AbstractSimpleTemplate<TestConfig, TestApplyPayload, TestRollbackPayload> {

public TestTemplateWithAdditionalClasses() {
super(AdditionalClass.class, AnotherAdditionalClass.class);
}

@Apply
public void apply() {
// Test implementation
}
}

// Test template with Void configuration
public static class TestTemplateWithVoidConfig
extends AbstractSimpleTemplate<Void, String, String> {

public TestTemplateWithVoidConfig() {
super();
}

@Apply
public void apply() {
// Test implementation
}
}

@Test
@DisplayName("getReflectiveClasses should return set containing configuration class")
void getReflectiveClassesShouldContainConfigurationClass() {
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.contains(TestConfig.class),
"Should contain configuration class TestConfig");
}

@Test
@DisplayName("getReflectiveClasses should return set containing apply payload class")
void getReflectiveClassesShouldContainApplyPayloadClass() {
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.contains(TestApplyPayload.class),
"Should contain apply payload class TestApplyPayload");
}

@Test
@DisplayName("getReflectiveClasses should return set containing rollback payload class")
void getReflectiveClassesShouldContainRollbackPayloadClass() {
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.contains(TestRollbackPayload.class),
"Should contain rollback payload class TestRollbackPayload");
}

@Test
@DisplayName("getReflectiveClasses should return set containing TemplateStep class")
void getReflectiveClassesShouldContainTemplateStepClass() {
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.contains(TemplateStep.class),
"Should contain TemplateStep class");
}

@Test
@DisplayName("getReflectiveClasses should include additional reflective classes passed to constructor")
void getReflectiveClassesShouldIncludeAdditionalClasses() {
TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.contains(AdditionalClass.class),
"Should contain AdditionalClass");
assertTrue(reflectiveClasses.contains(AnotherAdditionalClass.class),
"Should contain AnotherAdditionalClass");
}

@Test
@DisplayName("Multiple calls to getReflectiveClasses should return equivalent sets")
void multipleCallsShouldReturnEquivalentSets() {
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();

Collection<Class<?>> firstCall = template.getReflectiveClasses();
Collection<Class<?>> secondCall = template.getReflectiveClasses();

assertEquals(firstCall.size(), secondCall.size(),
"Both calls should return sets of the same size");
assertTrue(firstCall.containsAll(secondCall),
"First call should contain all elements of second call");
assertTrue(secondCall.containsAll(firstCall),
"Second call should contain all elements of first call");
}

@Test
@DisplayName("getReflectiveClasses with Void configuration should include Void class")
void getReflectiveClassesWithVoidConfigShouldIncludeVoidClass() {
TestTemplateWithVoidConfig template = new TestTemplateWithVoidConfig();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.contains(Void.class),
"Should contain Void class for configuration");
assertTrue(reflectiveClasses.contains(String.class),
"Should contain String class for apply/rollback payloads");
}

@Test
@DisplayName("getReflectiveClasses should return at least 4 classes (config, apply, rollback, TemplateStep)")
void getReflectiveClassesShouldReturnAtLeast4Classes() {
TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.size() >= 4,
"Should return at least 4 classes (config, apply, rollback, TemplateStep)");
}

@Test
@DisplayName("getReflectiveClasses with additional classes should return more than 4 classes")
void getReflectiveClassesWithAdditionalClassesShouldReturnMoreThan4() {
TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses();

Collection<Class<?>> reflectiveClasses = template.getReflectiveClasses();

assertTrue(reflectiveClasses.size() >= 6,
"Should return at least 6 classes (config, apply, rollback, TemplateStep, + 2 additional)");
}
}
Loading
Loading