Skip to content

Commit 33c0b97

Browse files
feat: add strict stages mapping validation (#741)
1 parent f963c28 commit 33c0b97

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,12 @@
209209
* @see SetupType
210210
*/
211211
SetupType setup() default SetupType.DEFAULT;
212+
213+
/**
214+
* If true, the annotation processor will validate that all code-based changes
215+
* (classes annotated with @Change) are mapped to some stage. When unmapped changes
216+
* are found and this flag is true, a RuntimeException is thrown. Default is false
217+
* (only a warning is emitted).
218+
*/
219+
boolean strictStageMapping() default false;
212220
}

core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import java.util.List;
5252
import java.util.Map;
5353
import java.util.Set;
54+
import java.util.function.BiPredicate;
5455
import java.util.stream.Collectors;
5556

5657
/**
@@ -209,6 +210,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
209210
noLegacyChangesByPackage,
210211
flamingockAnnotation
211212
);
213+
214+
validateAllChangesAreMappedToStages(legacyChanges, noLegacyChangesByPackage, pipeline, flamingockAnnotation.strictStageMapping());
215+
212216
Serializer serializer = new Serializer(processingEnv, logger);
213217
String setup = flamingockAnnotation.setup().toString();
214218
String configFile = flamingockAnnotation.configFile();
@@ -721,4 +725,92 @@ private void validateConfiguration(EnableFlamingock pipelineAnnotation, boolean
721725
}
722726
}
723727

728+
/**
729+
* Validates that code-based preview changes (non-legacy) are attached to some stage.
730+
* It checks package names from the changes map against the pipeline stages' sourcesPackage
731+
* (including the SYSTEM stage). If any stage does not represent any package, a RuntimeException is thrown.
732+
*
733+
* @param legacyCodedChanges legacy code changes (kept for completeness; not used for package mapping)
734+
* @param noLegacyCodedChangesByPackage non-legacy changes grouped by source package
735+
* @param pipeline built preview pipeline
736+
* @param strictStageMapping if true, unmapped changes will throw an exception; if false, will log a warning
737+
*/
738+
private void validateAllChangesAreMappedToStages(List<CodePreviewChange> legacyCodedChanges,
739+
Map<String, List<CodePreviewChange>> noLegacyCodedChangesByPackage,
740+
PreviewPipeline pipeline,
741+
Boolean strictStageMapping) {
742+
if (noLegacyCodedChangesByPackage == null || noLegacyCodedChangesByPackage.isEmpty()) {
743+
return;
744+
}
745+
746+
// Helper to test if a change package is covered by a stage package (exact or parent)
747+
BiPredicate<String, String> covers = (stagePkg, changePkg) -> {
748+
if (stagePkg == null || changePkg == null) return false;
749+
return changePkg.equals(stagePkg) || changePkg.startsWith(stagePkg + ".");
750+
};
751+
752+
List<String> unmapped = new ArrayList<>();
753+
754+
for (String pkg : noLegacyCodedChangesByPackage.keySet()) {
755+
if (pkg == null) {
756+
continue;
757+
}
758+
boolean matched = false;
759+
760+
// Check system stage
761+
PreviewStage system = pipeline.getSystemStage();
762+
if (system != null) {
763+
String sysPkg = system.getSourcesPackage();
764+
if (covers.test(sysPkg, pkg)) {
765+
matched = true;
766+
} else if (system.getTasks() != null) {
767+
for (io.flamingock.internal.common.core.preview.AbstractPreviewTask task : system.getTasks()) {
768+
if (task instanceof CodePreviewChange) {
769+
String taskPkg = ((CodePreviewChange) task).getSourcePackage();
770+
if (covers.test(taskPkg, pkg)) {
771+
matched = true;
772+
break;
773+
}
774+
}
775+
}
776+
}
777+
}
778+
779+
// Check regular stages
780+
if (!matched && pipeline.getStages() != null) {
781+
for (PreviewStage stage : pipeline.getStages()) {
782+
String stagePkg = stage.getSourcesPackage();
783+
if (covers.test(stagePkg, pkg)) {
784+
matched = true;
785+
break;
786+
}
787+
if (stage.getTasks() != null) {
788+
for (io.flamingock.internal.common.core.preview.AbstractPreviewTask task : stage.getTasks()) {
789+
if (task instanceof CodePreviewChange) {
790+
String taskPkg = ((CodePreviewChange) task).getSourcePackage();
791+
if (covers.test(taskPkg, pkg)) {
792+
matched = true;
793+
break;
794+
}
795+
}
796+
}
797+
if (matched) break;
798+
}
799+
}
800+
}
801+
802+
if (!matched) {
803+
unmapped.add(pkg);
804+
}
805+
}
806+
807+
if (!unmapped.isEmpty()) {
808+
String message = "Changes are not mapped to any stage: " + String.join(", ", unmapped);
809+
if (Boolean.TRUE.equals(strictStageMapping)) {
810+
throw new RuntimeException(message);
811+
} else {
812+
logger.warn(message);
813+
}
814+
}
815+
}
724816
}

core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
import java.io.File;
3333
import java.io.IOException;
34+
import java.lang.reflect.InvocationTargetException;
35+
import java.lang.reflect.Method;
3436
import java.nio.file.Files;
3537
import java.nio.file.Path;
3638
import java.util.Collections;
@@ -391,6 +393,47 @@ void shouldThrowErrorForMultipleSystemStagesInYaml() throws Exception {
391393
}
392394
}
393395

396+
@Test
397+
@DisplayName("Should throw error when changes exist that are not mapped to any stage and mapping is strict")
398+
void shouldThrowErrorForUnmappedChanges() throws Exception {
399+
FlamingockAnnotationProcessor processor = new FlamingockAnnotationProcessor();
400+
401+
// Prepare changes map with one mapped and one unmapped package (lists can be empty for this test)
402+
Map<String, List<io.flamingock.internal.common.core.preview.CodePreviewChange>> changesByPackage = new HashMap<>();
403+
changesByPackage.put("com.example.migrations", Collections.emptyList());
404+
changesByPackage.put("com.example.unmapped", Collections.emptyList()); // Unmapped
405+
406+
// Provide a non-empty changes list for the mapped stage so the builder doesn't throw
407+
io.flamingock.internal.common.core.preview.CodePreviewChange mockChange = new io.flamingock.internal.common.core.preview.CodePreviewChange();
408+
409+
// Build a pipeline with a single stage covering com.example.migrations
410+
PreviewStage mappedStage = PreviewStage.defaultBuilder(StageType.DEFAULT)
411+
.setName("migrations")
412+
.setSourcesPackage("com.example.migrations")
413+
.setSourcesRoots(Collections.emptyList())
414+
.setResourcesRoot("src/main/resources")
415+
.setChanges(Collections.singletonList(mockChange)) // <- non-empty list
416+
.build();
417+
418+
PreviewPipeline pipeline = new PreviewPipeline(Collections.singletonList(mappedStage));
419+
420+
// Invoke private validator by reflection and assert it throws (InvocationTargetException wraps the RuntimeException)
421+
Method validator = FlamingockAnnotationProcessor.class.getDeclaredMethod(
422+
"validateAllChangesAreMappedToStages", List.class, Map.class, PreviewPipeline.class, Boolean.class);
423+
validator.setAccessible(true);
424+
425+
InvocationTargetException ex = assertThrows(InvocationTargetException.class, () ->
426+
validator.invoke(processor, Collections.emptyList(), changesByPackage, pipeline, true));
427+
428+
Throwable cause = ex.getCause();
429+
assertNotNull(cause, "Validator should throw a RuntimeException cause");
430+
assertInstanceOf(RuntimeException.class, cause, "Cause should be RuntimeException");
431+
assertTrue(cause.getMessage().contains("not mapped to any stage"), "Should have error about unmapped changes");
432+
assertTrue(cause.getMessage().contains("com.example.unmapped"), "Should mention the unmapped package");
433+
}
434+
435+
436+
394437
// Helper methods using reflection to test the internal pipeline building logic
395438
private PreviewPipeline buildPipelineFromAnnotation(FlamingockAnnotationProcessor processor, EnableFlamingock annotation, Map<String, List<AbstractPreviewTask>> changes) throws Exception {
396439
// Set up minimal processor state
@@ -585,6 +628,12 @@ public EnableFlamingock build() {
585628
@Override public Stage[] stages() { return stages; }
586629
@Override public String configFile() { return configFile; }
587630
@Override public io.flamingock.api.SetupType setup() { return io.flamingock.api.SetupType.DEFAULT; }
631+
632+
@Override
633+
public boolean strictStageMapping() {
634+
return true;
635+
}
636+
588637
@Override public Class<? extends java.lang.annotation.Annotation> annotationType() { return EnableFlamingock.class; }
589638
};
590639
}

0 commit comments

Comments
 (0)