Skip to content

Commit 8b666a3

Browse files
vanroguclaude
andcommitted
Detect feature packages dynamically via FeatureSliceConfiguration instead of hardcoded package pattern
The ArchUnit rule no longer requires features to live under a `*.features.*` package. Instead, any package containing a concrete class implementing FeatureSliceConfiguration (directly or via extending) is treated as a feature package. Interfaces extending FeatureSliceConfiguration are excluded so shared interface packages are not flagged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 21cc15b commit 8b666a3

File tree

1 file changed

+78
-47
lines changed

1 file changed

+78
-47
lines changed

sliceworkz-eventmodeling-testing/src/main/java/org/sliceworkz/eventmodeling/testing/AbstractBoundedContextArchUnitTest.java

Lines changed: 78 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919

2020
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
2121

22-
import java.util.regex.Pattern;
22+
import java.util.HashSet;
23+
import java.util.Set;
2324

2425
import org.junit.jupiter.api.Test;
26+
import org.sliceworkz.eventmodeling.slices.FeatureSliceConfiguration;
2527

28+
import com.tngtech.archunit.base.DescribedPredicate;
2629
import com.tngtech.archunit.core.domain.JavaClass;
2730
import com.tngtech.archunit.core.domain.JavaClasses;
2831
import com.tngtech.archunit.core.importer.ClassFileImporter;
@@ -32,81 +35,109 @@
3235
import com.tngtech.archunit.lang.SimpleConditionEvent;
3336

3437
/**
35-
* Abstract Base Test to be overriden by a concrete Test Class in a root package for the overall application.
36-
* Checks
37-
38+
* Abstract Base Test to be overridden by a concrete Test Class in a root package for the overall application.
39+
* Checks
40+
*
3841
* VSA - Vertical Slice Architecture:
39-
* - whether no feature packages (*.features._featureName_.*) depends on another one.
40-
*
41-
*
42-
*
42+
* - whether no feature packages depend on another one.
43+
*
44+
* A feature package is any package that contains a class implementing
45+
* {@link FeatureSliceConfiguration} (directly or via extending). Classes in
46+
* subpackages of a feature package are considered part of the same feature.
47+
*
4348
* Overriding is done with an empty class, in the right base package.
44-
* (see "CoursesArchUnitTest" in the "courses" example for reference)
45-
*
49+
* (see "CoursesArchUnitTest" in the "courses" example for reference)
50+
*
4651
* package com.mycompany.mycomponent;
47-
*
52+
*
4853
* public class MyDomainArchUnitTest extends AbstractBoundedContextArchUnitTest {
49-
*
54+
*
5055
* }
51-
*
56+
*
5257
*/
5358
public abstract class AbstractBoundedContextArchUnitTest {
5459

5560
private JavaClasses CLASSES;
56-
61+
5762
public AbstractBoundedContextArchUnitTest ( ) {
5863
this.CLASSES = new ClassFileImporter().importPackages(packageName());
5964
}
60-
65+
6166
private String packageName ( ) {
6267
return this.getClass().getPackageName();
6368
}
6469

6570
@Test
6671
void featurePackagesShouldNotDependOnEachOther() {
67-
ArchRule rule = noClasses().that().resideInAPackage("..features..*").should(dependOnClassesFromOtherFeatures())
72+
Set<String> featurePackages = findFeaturePackages(CLASSES);
73+
74+
if (featurePackages.isEmpty()) {
75+
return;
76+
}
77+
78+
ArchRule rule = noClasses()
79+
.that(resideInFeaturePackages(featurePackages))
80+
.should(dependOnClassesFromOtherFeaturePackages(featurePackages))
6881
.because("Features should be independent and not depend on classes in other feature packages");
6982

7083
rule.check(CLASSES);
7184
}
7285

73-
private static ArchCondition<JavaClass> dependOnClassesFromOtherFeatures() {
74-
return new ArchCondition<JavaClass>("depend on classes from other features") {
75-
@Override
76-
public void check(JavaClass javaClass, ConditionEvents events) {
77-
System.err.println("checking " + javaClass);
86+
private static Set<String> findFeaturePackages(JavaClasses classes) {
87+
Set<String> featurePackages = new HashSet<>();
88+
for (JavaClass javaClass : classes) {
89+
if (!javaClass.isInterface() && javaClass.isAssignableTo(FeatureSliceConfiguration.class)) {
90+
featurePackages.add(javaClass.getPackageName());
91+
}
92+
}
93+
return featurePackages;
94+
}
7895

79-
if (javaClass.toString().contains("ActivateClockCom")) {
80-
System.err.println("w");
96+
private static String findOwningFeaturePackage(String packageName, Set<String> featurePackages) {
97+
String best = null;
98+
for (String featurePackage : featurePackages) {
99+
if (packageName.equals(featurePackage) || packageName.startsWith(featurePackage + ".")) {
100+
if (best == null || featurePackage.length() > best.length()) {
101+
best = featurePackage;
81102
}
103+
}
104+
}
105+
return best;
106+
}
82107

83-
Pattern featurePattern = Pattern.compile(".*\\.features\\.([^.]+)");
84-
java.util.regex.Matcher currentFeatureMatcher = featurePattern.matcher(javaClass.getPackageName());
108+
private static DescribedPredicate<JavaClass> resideInFeaturePackages(Set<String> featurePackages) {
109+
return new DescribedPredicate<>("reside in a feature package") {
110+
@Override
111+
public boolean test(JavaClass javaClass) {
112+
return findOwningFeaturePackage(javaClass.getPackageName(), featurePackages) != null;
113+
}
114+
};
115+
}
85116

86-
if (!currentFeatureMatcher.find()) {
87-
return; // Not in a feature package
117+
private static ArchCondition<JavaClass> dependOnClassesFromOtherFeaturePackages(Set<String> featurePackages) {
118+
return new ArchCondition<>("depend on classes from other feature packages") {
119+
@Override
120+
public void check(JavaClass javaClass, ConditionEvents events) {
121+
String currentFeature = findOwningFeaturePackage(javaClass.getPackageName(), featurePackages);
122+
if (currentFeature == null) {
123+
return;
88124
}
89125

90-
String currentFeature = currentFeatureMatcher.group(1);
91-
92-
javaClass.getDirectDependenciesFromSelf().stream().filter(dependency -> {
93-
String targetPackage = dependency.getTargetClass().getPackageName();
94-
java.util.regex.Matcher targetMatcher = featurePattern.matcher(targetPackage);
95-
if (!targetMatcher.find()) {
96-
return false; // Target not in features package
97-
}
98-
String targetFeature = targetMatcher.group(1);
99-
if (currentFeature.equals(targetFeature)) {
100-
return false; // Same feature - allowed
101-
}
102-
103-
return true;
104-
105-
}).forEach(dependency -> {
106-
String message = "Class %s depends on %s from different feature".formatted(javaClass.getName(),
107-
dependency.getTargetClass().getName());
108-
events.add(SimpleConditionEvent.satisfied(dependency, message));
109-
});
126+
javaClass.getDirectDependenciesFromSelf().stream()
127+
.filter(dependency -> {
128+
String targetFeature = findOwningFeaturePackage(
129+
dependency.getTargetClass().getPackageName(), featurePackages);
130+
if (targetFeature == null) {
131+
return false;
132+
}
133+
return !currentFeature.equals(targetFeature);
134+
})
135+
.forEach(dependency -> {
136+
String message = "Class %s depends on %s from different feature package".formatted(
137+
javaClass.getName(),
138+
dependency.getTargetClass().getName());
139+
events.add(SimpleConditionEvent.satisfied(dependency, message));
140+
});
110141
}
111142
};
112143
}

0 commit comments

Comments
 (0)