-
Notifications
You must be signed in to change notification settings - Fork 299
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
156 changed files
with
7,108 additions
and
990 deletions.
There are no files selected for viewing
193 changes: 193 additions & 0 deletions
193
...ple/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ModulesTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
package com.tngtech.archunit.exampletest.junit4; | ||
|
||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.function.Supplier; | ||
|
||
import com.tngtech.archunit.base.DescribedFunction; | ||
import com.tngtech.archunit.base.DescribedPredicate; | ||
import com.tngtech.archunit.core.domain.JavaClass; | ||
import com.tngtech.archunit.core.domain.JavaPackage; | ||
import com.tngtech.archunit.example.AppModule; | ||
import com.tngtech.archunit.example.ModuleApi; | ||
import com.tngtech.archunit.junit.AnalyzeClasses; | ||
import com.tngtech.archunit.junit.ArchTest; | ||
import com.tngtech.archunit.junit.ArchUnitRunner; | ||
import com.tngtech.archunit.lang.ArchRule; | ||
import com.tngtech.archunit.library.modules.AnnotationDescriptor; | ||
import com.tngtech.archunit.library.modules.ArchModule; | ||
import com.tngtech.archunit.library.modules.ModuleDependency; | ||
import com.tngtech.archunit.library.modules.syntax.DescriptorFunction; | ||
import org.junit.experimental.categories.Category; | ||
import org.junit.runner.RunWith; | ||
|
||
import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; | ||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; | ||
import static com.tngtech.archunit.library.modules.syntax.AllowedModuleDependencies.allow; | ||
import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage; | ||
import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; | ||
import static java.util.Arrays.stream; | ||
import static java.util.stream.Collectors.toList; | ||
|
||
@Category(Example.class) | ||
@RunWith(ArchUnitRunner.class) | ||
@AnalyzeClasses(packages = "com.tngtech.archunit.example") | ||
public class ModulesTest { | ||
|
||
/** | ||
* This example demonstrates how to derive modules from a package pattern. | ||
* The `..` stands for arbitrary many packages and the `(*)` captures one specific subpackage name within the | ||
* package tree. | ||
*/ | ||
@ArchTest | ||
public static ArchRule modules_should_respect_their_declared_dependencies__use_package_API = | ||
modules() | ||
.definedByPackages("..shopping.(*)..") | ||
.should().respectTheirAllowedDependencies( | ||
allow() | ||
.fromModule("catalog").toModules("product") | ||
.fromModule("customer").toModules("address") | ||
.fromModule("importer").toModules("catalog", "xml") | ||
.fromModule("order").toModules("customer", "product"), | ||
consideringOnlyDependenciesInAnyPackage("..example..")) | ||
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); | ||
|
||
/** | ||
* This example demonstrates how to easily derive modules from classes annotated with a certain annotation, | ||
* and also test for allowed dependencies and correct access to exposed packages by declared descriptor annotation properties. | ||
* Within the example those are simply package-info files which denote the root of the modules by | ||
* being annotated with @AppModule. | ||
*/ | ||
@ArchTest | ||
public static ArchRule modules_should_respect_their_declared_dependencies_and_exposed_packages = | ||
modules() | ||
.definedByAnnotation(AppModule.class) | ||
.should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", | ||
consideringOnlyDependenciesInAnyPackage("..example..")) | ||
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) | ||
.andShould().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages"); | ||
|
||
/** | ||
* This example demonstrates how to easily derive modules from classes annotated with a certain annotation, | ||
* and also test for allowed dependencies using the descriptor annotation. | ||
* Within the example those are simply package-info files which denote the root of the modules by | ||
* being annotated with @AppModule. | ||
*/ | ||
@ArchTest | ||
public static ArchRule modules_should_respect_their_declared_dependencies__use_annotation_API = | ||
modules() | ||
.definedByAnnotation(AppModule.class) | ||
.should().respectTheirAllowedDependencies( | ||
declaredByDescriptorAnnotation(), | ||
consideringOnlyDependenciesInAnyPackage("..example..") | ||
) | ||
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); | ||
|
||
/** | ||
* This example demonstrates how to use the slightly more generic root class API to define modules. | ||
* While the result in this example is the same as the above, this API in general can be used to | ||
* use arbitrary classes as roots of modules. | ||
* For example if there is always a central interface denoted in some way, | ||
* the modules could be derived from these interfaces. | ||
*/ | ||
@ArchTest | ||
public static ArchRule modules_should_respect_their_declared_dependencies__use_root_class_API = | ||
modules() | ||
.definedByRootClasses( | ||
DescribedPredicate.describe("annotated with @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> | ||
rootClass.isAnnotatedWith(AppModule.class)) | ||
) | ||
.derivingModuleFromRootClassBy( | ||
DescribedFunction.describe("annotation @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> { | ||
AppModule module = rootClass.getAnnotationOfType(AppModule.class); | ||
return new AnnotationDescriptor<>(module.name(), module); | ||
}) | ||
) | ||
.should().respectTheirAllowedDependencies( | ||
declaredByDescriptorAnnotation(), | ||
consideringOnlyDependenciesInAnyPackage("..example..") | ||
) | ||
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); | ||
|
||
/** | ||
* This example demonstrates how to use the generic API to define modules. | ||
* The result in this example again is the same as the above, however in general the generic API | ||
* allows to derive modules in a completely customizable way. | ||
*/ | ||
@ArchTest | ||
public static ArchRule modules_should_respect_their_declared_dependencies__use_generic_API = | ||
modules() | ||
.definedBy(identifierFromModulesAnnotation()) | ||
.derivingModule(fromModulesAnnotation()) | ||
.should().respectTheirAllowedDependencies( | ||
declaredByDescriptorAnnotation(), | ||
consideringOnlyDependenciesInAnyPackage("..example..") | ||
) | ||
.ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); | ||
|
||
/** | ||
* This example demonstrates how to check that modules only depend on each other through a specific API. | ||
*/ | ||
@ArchTest | ||
public static ArchRule modules_should_only_depend_on_each_other_through_module_API = | ||
modules() | ||
.definedByAnnotation(AppModule.class) | ||
.should().onlyDependOnEachOtherThroughClassesThat().areAnnotatedWith(ModuleApi.class); | ||
|
||
/** | ||
* This example demonstrates how to check for cyclic dependencies between modules. | ||
*/ | ||
@ArchTest | ||
public static ArchRule modules_should_be_free_of_cycles = | ||
modules() | ||
.definedByAnnotation(AppModule.class) | ||
.should().beFreeOfCycles(); | ||
|
||
private static DescribedPredicate<ModuleDependency<AnnotationDescriptor<AppModule>>> declaredByDescriptorAnnotation() { | ||
return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { | ||
AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); | ||
List<String> allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); | ||
return allowedDependencies.contains(moduleDependency.getTarget().getName()); | ||
}); | ||
} | ||
|
||
private static IdentifierFromAnnotation identifierFromModulesAnnotation() { | ||
return new IdentifierFromAnnotation(); | ||
} | ||
|
||
private static DescriptorFunction<AnnotationDescriptor<AppModule>> fromModulesAnnotation() { | ||
return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), | ||
(ArchModule.Identifier identifier, Set<JavaClass> containedClasses) -> { | ||
JavaClass rootClass = containedClasses.stream().filter(it -> it.isAnnotatedWith(AppModule.class)).findFirst().get(); | ||
AppModule module = rootClass.getAnnotationOfType(AppModule.class); | ||
return new AnnotationDescriptor<>(module.name(), module); | ||
}); | ||
} | ||
|
||
private static class IdentifierFromAnnotation extends DescribedFunction<JavaClass, ArchModule.Identifier> { | ||
IdentifierFromAnnotation() { | ||
super("root classes with annotation @" + AppModule.class.getSimpleName()); | ||
} | ||
|
||
@Override | ||
public ArchModule.Identifier apply(JavaClass javaClass) { | ||
return getIdentifierOfPackage(javaClass.getPackage()); | ||
} | ||
|
||
private ArchModule.Identifier getIdentifierOfPackage(JavaPackage javaPackage) { | ||
Optional<ArchModule.Identifier> identifierInCurrentPackage = javaPackage.getClasses().stream() | ||
.filter(it -> it.isAnnotatedWith(AppModule.class)) | ||
.findFirst() | ||
.map(annotatedClassInPackage -> ArchModule.Identifier.from(annotatedClassInPackage.getAnnotationOfType(AppModule.class).name())); | ||
|
||
return identifierInCurrentPackage.orElseGet(identifierInParentPackageOf(javaPackage)); | ||
} | ||
|
||
private Supplier<ArchModule.Identifier> identifierInParentPackageOf(JavaPackage javaPackage) { | ||
return () -> javaPackage.getParent() | ||
.map(this::getIdentifierOfPackage) | ||
.orElseGet(ArchModule.Identifier::ignore); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.