Skip to content

Commit 0e90343

Browse files
authored
Add modules syntax to rules API (#1078)
This PR will add a convenient fluent rules API to partition `JavaClasses` into (architecture) modules and assert conditions on this module structure. The basic syntax has the form ``` modules() .definedBy... // use one of the various methods to decide how to partition classes into modules .should()... // evaluate some predefined or custom condition ``` While some concepts are quite similar to the `Slices` API I've noticed over the years that most people don't see the possibilities to use the `Slices` API to check modularization properties. Thus, I decided to not make the existing API more powerful, but instead create this new API which offers most features of the `Slices` API (e.g. create from package identifiers, check for cycles) but also more powerful / concise possibilities to introduce modularization into a code base (e.g. by an annotated `package-info`) and carry meta-information like allowed dependencies from such an annotated object into the modules rule.
2 parents 9759d7f + 2cdb316 commit 0e90343

156 files changed

Lines changed: 7108 additions & 990 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.tngtech.archunit.exampletest.junit4;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
import java.util.Set;
6+
import java.util.function.Supplier;
7+
8+
import com.tngtech.archunit.base.DescribedFunction;
9+
import com.tngtech.archunit.base.DescribedPredicate;
10+
import com.tngtech.archunit.core.domain.JavaClass;
11+
import com.tngtech.archunit.core.domain.JavaPackage;
12+
import com.tngtech.archunit.example.AppModule;
13+
import com.tngtech.archunit.example.ModuleApi;
14+
import com.tngtech.archunit.junit.AnalyzeClasses;
15+
import com.tngtech.archunit.junit.ArchTest;
16+
import com.tngtech.archunit.junit.ArchUnitRunner;
17+
import com.tngtech.archunit.lang.ArchRule;
18+
import com.tngtech.archunit.library.modules.AnnotationDescriptor;
19+
import com.tngtech.archunit.library.modules.ArchModule;
20+
import com.tngtech.archunit.library.modules.ModuleDependency;
21+
import com.tngtech.archunit.library.modules.syntax.DescriptorFunction;
22+
import org.junit.experimental.categories.Category;
23+
import org.junit.runner.RunWith;
24+
25+
import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue;
26+
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf;
27+
import static com.tngtech.archunit.library.modules.syntax.AllowedModuleDependencies.allow;
28+
import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage;
29+
import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules;
30+
import static java.util.Arrays.stream;
31+
import static java.util.stream.Collectors.toList;
32+
33+
@Category(Example.class)
34+
@RunWith(ArchUnitRunner.class)
35+
@AnalyzeClasses(packages = "com.tngtech.archunit.example")
36+
public class ModulesTest {
37+
38+
/**
39+
* This example demonstrates how to derive modules from a package pattern.
40+
* The `..` stands for arbitrary many packages and the `(*)` captures one specific subpackage name within the
41+
* package tree.
42+
*/
43+
@ArchTest
44+
public static ArchRule modules_should_respect_their_declared_dependencies__use_package_API =
45+
modules()
46+
.definedByPackages("..shopping.(*)..")
47+
.should().respectTheirAllowedDependencies(
48+
allow()
49+
.fromModule("catalog").toModules("product")
50+
.fromModule("customer").toModules("address")
51+
.fromModule("importer").toModules("catalog", "xml")
52+
.fromModule("order").toModules("customer", "product"),
53+
consideringOnlyDependenciesInAnyPackage("..example.."))
54+
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class));
55+
56+
/**
57+
* This example demonstrates how to easily derive modules from classes annotated with a certain annotation,
58+
* and also test for allowed dependencies and correct access to exposed packages by declared descriptor annotation properties.
59+
* Within the example those are simply package-info files which denote the root of the modules by
60+
* being annotated with @AppModule.
61+
*/
62+
@ArchTest
63+
public static ArchRule modules_should_respect_their_declared_dependencies_and_exposed_packages =
64+
modules()
65+
.definedByAnnotation(AppModule.class)
66+
.should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies",
67+
consideringOnlyDependenciesInAnyPackage("..example.."))
68+
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class))
69+
.andShould().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages");
70+
71+
/**
72+
* This example demonstrates how to easily derive modules from classes annotated with a certain annotation,
73+
* and also test for allowed dependencies using the descriptor annotation.
74+
* Within the example those are simply package-info files which denote the root of the modules by
75+
* being annotated with @AppModule.
76+
*/
77+
@ArchTest
78+
public static ArchRule modules_should_respect_their_declared_dependencies__use_annotation_API =
79+
modules()
80+
.definedByAnnotation(AppModule.class)
81+
.should().respectTheirAllowedDependencies(
82+
declaredByDescriptorAnnotation(),
83+
consideringOnlyDependenciesInAnyPackage("..example..")
84+
)
85+
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class));
86+
87+
/**
88+
* This example demonstrates how to use the slightly more generic root class API to define modules.
89+
* While the result in this example is the same as the above, this API in general can be used to
90+
* use arbitrary classes as roots of modules.
91+
* For example if there is always a central interface denoted in some way,
92+
* the modules could be derived from these interfaces.
93+
*/
94+
@ArchTest
95+
public static ArchRule modules_should_respect_their_declared_dependencies__use_root_class_API =
96+
modules()
97+
.definedByRootClasses(
98+
DescribedPredicate.describe("annotated with @" + AppModule.class.getSimpleName(), (JavaClass rootClass) ->
99+
rootClass.isAnnotatedWith(AppModule.class))
100+
)
101+
.derivingModuleFromRootClassBy(
102+
DescribedFunction.describe("annotation @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> {
103+
AppModule module = rootClass.getAnnotationOfType(AppModule.class);
104+
return new AnnotationDescriptor<>(module.name(), module);
105+
})
106+
)
107+
.should().respectTheirAllowedDependencies(
108+
declaredByDescriptorAnnotation(),
109+
consideringOnlyDependenciesInAnyPackage("..example..")
110+
)
111+
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class));
112+
113+
/**
114+
* This example demonstrates how to use the generic API to define modules.
115+
* The result in this example again is the same as the above, however in general the generic API
116+
* allows to derive modules in a completely customizable way.
117+
*/
118+
@ArchTest
119+
public static ArchRule modules_should_respect_their_declared_dependencies__use_generic_API =
120+
modules()
121+
.definedBy(identifierFromModulesAnnotation())
122+
.derivingModule(fromModulesAnnotation())
123+
.should().respectTheirAllowedDependencies(
124+
declaredByDescriptorAnnotation(),
125+
consideringOnlyDependenciesInAnyPackage("..example..")
126+
)
127+
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class));
128+
129+
/**
130+
* This example demonstrates how to check that modules only depend on each other through a specific API.
131+
*/
132+
@ArchTest
133+
public static ArchRule modules_should_only_depend_on_each_other_through_module_API =
134+
modules()
135+
.definedByAnnotation(AppModule.class)
136+
.should().onlyDependOnEachOtherThroughClassesThat().areAnnotatedWith(ModuleApi.class);
137+
138+
/**
139+
* This example demonstrates how to check for cyclic dependencies between modules.
140+
*/
141+
@ArchTest
142+
public static ArchRule modules_should_be_free_of_cycles =
143+
modules()
144+
.definedByAnnotation(AppModule.class)
145+
.should().beFreeOfCycles();
146+
147+
private static DescribedPredicate<ModuleDependency<AnnotationDescriptor<AppModule>>> declaredByDescriptorAnnotation() {
148+
return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> {
149+
AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation();
150+
List<String> allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList());
151+
return allowedDependencies.contains(moduleDependency.getTarget().getName());
152+
});
153+
}
154+
155+
private static IdentifierFromAnnotation identifierFromModulesAnnotation() {
156+
return new IdentifierFromAnnotation();
157+
}
158+
159+
private static DescriptorFunction<AnnotationDescriptor<AppModule>> fromModulesAnnotation() {
160+
return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()),
161+
(ArchModule.Identifier identifier, Set<JavaClass> containedClasses) -> {
162+
JavaClass rootClass = containedClasses.stream().filter(it -> it.isAnnotatedWith(AppModule.class)).findFirst().get();
163+
AppModule module = rootClass.getAnnotationOfType(AppModule.class);
164+
return new AnnotationDescriptor<>(module.name(), module);
165+
});
166+
}
167+
168+
private static class IdentifierFromAnnotation extends DescribedFunction<JavaClass, ArchModule.Identifier> {
169+
IdentifierFromAnnotation() {
170+
super("root classes with annotation @" + AppModule.class.getSimpleName());
171+
}
172+
173+
@Override
174+
public ArchModule.Identifier apply(JavaClass javaClass) {
175+
return getIdentifierOfPackage(javaClass.getPackage());
176+
}
177+
178+
private ArchModule.Identifier getIdentifierOfPackage(JavaPackage javaPackage) {
179+
Optional<ArchModule.Identifier> identifierInCurrentPackage = javaPackage.getClasses().stream()
180+
.filter(it -> it.isAnnotatedWith(AppModule.class))
181+
.findFirst()
182+
.map(annotatedClassInPackage -> ArchModule.Identifier.from(annotatedClassInPackage.getAnnotationOfType(AppModule.class).name()));
183+
184+
return identifierInCurrentPackage.orElseGet(identifierInParentPackageOf(javaPackage));
185+
}
186+
187+
private Supplier<ArchModule.Identifier> identifierInParentPackageOf(JavaPackage javaPackage) {
188+
return () -> javaPackage.getParent()
189+
.map(this::getIdentifierOfPackage)
190+
.orElseGet(ArchModule.Identifier::ignore);
191+
}
192+
}
193+
}

archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import java.net.URL;
44

55
import com.tngtech.archunit.core.domain.PackageMatchers;
6-
import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog;
7-
import com.tngtech.archunit.example.plantuml.order.Order;
8-
import com.tngtech.archunit.example.plantuml.product.Product;
6+
import com.tngtech.archunit.example.shopping.catalog.ProductCatalog;
7+
import com.tngtech.archunit.example.shopping.order.Order;
8+
import com.tngtech.archunit.example.shopping.product.Product;
99
import com.tngtech.archunit.junit.AnalyzeClasses;
1010
import com.tngtech.archunit.junit.ArchTest;
1111
import com.tngtech.archunit.junit.ArchUnitRunner;
@@ -24,7 +24,7 @@
2424

2525
@Category(Example.class)
2626
@RunWith(ArchUnitRunner.class)
27-
@AnalyzeClasses(packages = "com.tngtech.archunit.example.plantuml")
27+
@AnalyzeClasses(packages = "com.tngtech.archunit.example.shopping")
2828
public class PlantUmlArchitectureTest {
2929
private static final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml");
3030

0 commit comments

Comments
 (0)