|
19 | 19 |
|
20 | 20 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; |
21 | 21 |
|
22 | | -import java.util.regex.Pattern; |
| 22 | +import java.util.HashSet; |
| 23 | +import java.util.Set; |
23 | 24 |
|
24 | 25 | import org.junit.jupiter.api.Test; |
| 26 | +import org.sliceworkz.eventmodeling.slices.FeatureSliceConfiguration; |
25 | 27 |
|
| 28 | +import com.tngtech.archunit.base.DescribedPredicate; |
26 | 29 | import com.tngtech.archunit.core.domain.JavaClass; |
27 | 30 | import com.tngtech.archunit.core.domain.JavaClasses; |
28 | 31 | import com.tngtech.archunit.core.importer.ClassFileImporter; |
|
32 | 35 | import com.tngtech.archunit.lang.SimpleConditionEvent; |
33 | 36 |
|
34 | 37 | /** |
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 | + * |
38 | 41 | * 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 | + * |
43 | 48 | * 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 | + * |
46 | 51 | * package com.mycompany.mycomponent; |
47 | | - * |
| 52 | + * |
48 | 53 | * public class MyDomainArchUnitTest extends AbstractBoundedContextArchUnitTest { |
49 | | - * |
| 54 | + * |
50 | 55 | * } |
51 | | - * |
| 56 | + * |
52 | 57 | */ |
53 | 58 | public abstract class AbstractBoundedContextArchUnitTest { |
54 | 59 |
|
55 | 60 | private JavaClasses CLASSES; |
56 | | - |
| 61 | + |
57 | 62 | public AbstractBoundedContextArchUnitTest ( ) { |
58 | 63 | this.CLASSES = new ClassFileImporter().importPackages(packageName()); |
59 | 64 | } |
60 | | - |
| 65 | + |
61 | 66 | private String packageName ( ) { |
62 | 67 | return this.getClass().getPackageName(); |
63 | 68 | } |
64 | 69 |
|
65 | 70 | @Test |
66 | 71 | 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)) |
68 | 81 | .because("Features should be independent and not depend on classes in other feature packages"); |
69 | 82 |
|
70 | 83 | rule.check(CLASSES); |
71 | 84 | } |
72 | 85 |
|
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 | + } |
78 | 95 |
|
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; |
81 | 102 | } |
| 103 | + } |
| 104 | + } |
| 105 | + return best; |
| 106 | + } |
82 | 107 |
|
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 | + } |
85 | 116 |
|
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; |
88 | 124 | } |
89 | 125 |
|
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 | + }); |
110 | 141 | } |
111 | 142 | }; |
112 | 143 | } |
|
0 commit comments