diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ModulesTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ModulesTest.java new file mode 100644 index 0000000000..5db5b8447a --- /dev/null +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ModulesTest.java @@ -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>> declaredByDescriptorAnnotation() { + return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { + AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); + List allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }); + } + + private static IdentifierFromAnnotation identifierFromModulesAnnotation() { + return new IdentifierFromAnnotation(); + } + + private static DescriptorFunction> fromModulesAnnotation() { + return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), + (ArchModule.Identifier identifier, Set 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 { + 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 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 identifierInParentPackageOf(JavaPackage javaPackage) { + return () -> javaPackage.getParent() + .map(this::getIdentifierOfPackage) + .orElseGet(ArchModule.Identifier::ignore); + } + } +} diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java index de159cf331..f8cf2a70fe 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java @@ -3,9 +3,9 @@ import java.net.URL; import com.tngtech.archunit.core.domain.PackageMatchers; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.ArchUnitRunner; @@ -24,7 +24,7 @@ @Category(Example.class) @RunWith(ArchUnitRunner.class) -@AnalyzeClasses(packages = "com.tngtech.archunit.example.plantuml") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.shopping") public class PlantUmlArchitectureTest { private static final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml"); diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java new file mode 100644 index 0000000000..dde3b13576 --- /dev/null +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java @@ -0,0 +1,190 @@ +package com.tngtech.archunit.exampletest.junit5; + +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.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +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 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; + +@ArchTag("example") +@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 + 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 + 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 + 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 + 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 + 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 + 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 + static ArchRule modules_should_be_free_of_cycles = + modules() + .definedByAnnotation(AppModule.class) + .should().beFreeOfCycles(); + + private static DescribedPredicate>> declaredByDescriptorAnnotation() { + return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { + AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); + List allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }); + } + + private static IdentifierFromAnnotation identifierFromModulesAnnotation() { + return new IdentifierFromAnnotation(); + } + + private static DescriptorFunction> fromModulesAnnotation() { + return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), + (ArchModule.Identifier identifier, Set 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 { + 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 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 identifierInParentPackageOf(JavaPackage javaPackage) { + return () -> javaPackage.getParent() + .map(this::getIdentifierOfPackage) + .orElseGet(ArchModule.Identifier::ignore); + } + } +} diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java index 4a323f367f..db64b7d984 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java @@ -3,9 +3,9 @@ import java.net.URL; import com.tngtech.archunit.core.domain.PackageMatchers; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTag; import com.tngtech.archunit.junit.ArchTest; @@ -21,7 +21,7 @@ import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.adhereToPlantUmlDiagram; @ArchTag("example") -@AnalyzeClasses(packages = "com.tngtech.archunit.example.plantuml") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.shopping") public class PlantUmlArchitectureTest { private static final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml"); diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/AppModule.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/AppModule.java new file mode 100644 index 0000000000..289b2e2541 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/AppModule.java @@ -0,0 +1,9 @@ +package com.tngtech.archunit.example; + +public @interface AppModule { + String name(); + + String[] allowedDependencies() default {}; + + String[] exposedPackages() default {}; +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/ModuleApi.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/ModuleApi.java new file mode 100644 index 0000000000..cabcafc4aa --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/ModuleApi.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example; + +public @interface ModuleApi { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/address/Address.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/address/Address.java deleted file mode 100644 index 0ffe9c0c8b..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/address/Address.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tngtech.archunit.example.plantuml.address; - -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; - -public class Address { - private ProductCatalog productCatalog; -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/customer/Customer.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/customer/Customer.java deleted file mode 100644 index d8524dfee8..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/customer/Customer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.tngtech.archunit.example.plantuml.customer; - -import com.tngtech.archunit.example.plantuml.address.Address; -import com.tngtech.archunit.example.plantuml.order.Order; - -public class Customer { - private Address address; - - void addOrder(Order order) { - // simply having such a parameter violates the specified UML diagram - } - - public Address getAddress() { - return address; - } -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/importer/ProductImport.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/importer/ProductImport.java deleted file mode 100644 index 5862864668..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/importer/ProductImport.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.tngtech.archunit.example.plantuml.importer; - -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.xml.processor.XmlProcessor; -import com.tngtech.archunit.example.plantuml.xml.types.XmlTypes; - -public class ProductImport { - public ProductCatalog productCatalog; - public XmlTypes xmlType; - public XmlProcessor xmlProcessor; - - public Customer getCustomer() { - return new Customer(); // violates diagram -> product import may not directly know Customer - } - - ProductCatalog parse(byte[] xml) { - return new ProductCatalog(); - } -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/product/Product.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/product/Product.java deleted file mode 100644 index 14f1d4ca4b..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/product/Product.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.tngtech.archunit.example.plantuml.product; - -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.order.Order; - -public class Product { - public Customer customer; - - Order getOrder() { - return null; // the return type violates the specified UML diagram - } - - public void register() { - } - - public void report() { - } -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/processor/XmlProcessor.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/processor/XmlProcessor.java deleted file mode 100644 index 57f2a1d50c..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/processor/XmlProcessor.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tngtech.archunit.example.plantuml.xml.processor; - -public class XmlProcessor { -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/types/XmlTypes.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/types/XmlTypes.java deleted file mode 100644 index 1ba7ad07e3..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/types/XmlTypes.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tngtech.archunit.example.plantuml.xml.types; - -public class XmlTypes { -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/utils/XmlUtils.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/utils/XmlUtils.java deleted file mode 100644 index 8674a9c93f..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/utils/XmlUtils.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tngtech.archunit.example.plantuml.xml.utils; - -public class XmlUtils { -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/Address.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/Address.java new file mode 100644 index 0000000000..2f04a96ed8 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/Address.java @@ -0,0 +1,10 @@ +package com.tngtech.archunit.example.shopping.address; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; + +@ModuleApi +@SuppressWarnings("unused") +public class Address { + private ProductCatalog productCatalog; +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/AddressController.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/AddressController.java new file mode 100644 index 0000000000..c1460b32ad --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/AddressController.java @@ -0,0 +1,12 @@ +package com.tngtech.archunit.example.shopping.address; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.layers.AbstractController; + +@ModuleApi +@SuppressWarnings("unused") +public class AddressController extends AbstractController { + void handleAddress(Address address) { + // do something + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/package-info.java new file mode 100644 index 0000000000..33ebb21853 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "Address", + exposedPackages = "com.tngtech.archunit.example.shopping.address" +) +package com.tngtech.archunit.example.shopping.address; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/catalog/ProductCatalog.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/ProductCatalog.java similarity index 63% rename from archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/catalog/ProductCatalog.java rename to archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/ProductCatalog.java index 342e383f7b..d8c29f81cf 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/catalog/ProductCatalog.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/ProductCatalog.java @@ -1,9 +1,9 @@ -package com.tngtech.archunit.example.plantuml.catalog; +package com.tngtech.archunit.example.shopping.catalog; import java.util.Set; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; public class ProductCatalog { private Set allProducts; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/package-info.java new file mode 100644 index 0000000000..f5074454ea --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "Catalog", + allowedDependencies = {"Product"} +) +package com.tngtech.archunit.example.shopping.catalog; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/Customer.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/Customer.java new file mode 100644 index 0000000000..82af19e656 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/Customer.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.example.shopping.customer; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.address.Address; +import com.tngtech.archunit.example.shopping.order.Order; + +@ModuleApi +@SuppressWarnings("unused") +public class Customer { + private Address address; + + void addOrder(Order order) { + // simply having such a parameter violates the specified UML diagram + } + + public Address getAddress() { + return address; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/package-info.java new file mode 100644 index 0000000000..29d7754335 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/package-info.java @@ -0,0 +1,8 @@ +@AppModule( + name = "Customer", + allowedDependencies = {"Address"}, + exposedPackages = "com.tngtech.archunit.example.shopping.customer" +) +package com.tngtech.archunit.example.shopping.customer; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/ProductImport.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/ProductImport.java new file mode 100644 index 0000000000..8989528619 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/ProductImport.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.example.shopping.importer; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.xml.processor.XmlProcessor; +import com.tngtech.archunit.example.shopping.xml.types.XmlTypes; + +@ModuleApi +@SuppressWarnings("unused") +public class ProductImport { + public ProductCatalog productCatalog; + public XmlTypes xmlType; + public XmlProcessor xmlProcessor; + + public Customer getCustomer() { + return new Customer(); // violates diagram -> product import may not directly know Customer + } + + ProductCatalog parse(byte[] xml) { + return new ProductCatalog(); + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/package-info.java new file mode 100644 index 0000000000..457f44c654 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/package-info.java @@ -0,0 +1,8 @@ +@AppModule( + name = "Importer", + allowedDependencies = {"Catalog", "XML"}, + exposedPackages = "com.tngtech.archunit.example.shopping.importer" +) +package com.tngtech.archunit.example.shopping.importer; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/order/Order.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/Order.java similarity index 55% rename from archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/order/Order.java rename to archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/Order.java index cf7b5f756d..2e1ddc13c4 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/order/Order.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/Order.java @@ -1,11 +1,14 @@ -package com.tngtech.archunit.example.plantuml.order; +package com.tngtech.archunit.example.shopping.order; import java.util.Set; -import com.tngtech.archunit.example.plantuml.address.Address; -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.address.Address; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.product.Product; +@ModuleApi +@SuppressWarnings("unused") public class Order { public Customer customer; private Set products; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/package-info.java new file mode 100644 index 0000000000..350bd44e73 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/package-info.java @@ -0,0 +1,8 @@ +@AppModule( + name = "Order", + allowedDependencies = {"Customer", "Product"}, + exposedPackages = "com.tngtech.archunit.example.shopping.order" +) +package com.tngtech.archunit.example.shopping.order; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/Product.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/Product.java new file mode 100644 index 0000000000..709fb7a33f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/Product.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.example.shopping.product; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.order.Order; + +@ModuleApi +@SuppressWarnings("unused") +public class Product { + public Customer customer; + + Order getOrder() { + return null; // the return type violates the specified UML diagram + } + + public void register() { + } + + public void report() { + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/package-info.java new file mode 100644 index 0000000000..1404352f4c --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "Product", + exposedPackages = "com.tngtech.archunit.example.shopping.product" +) +package com.tngtech.archunit.example.shopping.product; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/package-info.java new file mode 100644 index 0000000000..2bd971cf42 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "XML", + exposedPackages = "com.tngtech.archunit.example.shopping.xml.processor" +) +package com.tngtech.archunit.example.shopping.xml; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/processor/XmlProcessor.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/processor/XmlProcessor.java new file mode 100644 index 0000000000..c1fdfef1e9 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/processor/XmlProcessor.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.example.shopping.xml.processor; + +import com.tngtech.archunit.example.ModuleApi; + +@ModuleApi +public class XmlProcessor { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/types/XmlTypes.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/types/XmlTypes.java new file mode 100644 index 0000000000..5569db3057 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/types/XmlTypes.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.shopping.xml.types; + +public class XmlTypes { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/utils/XmlUtils.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/utils/XmlUtils.java new file mode 100644 index 0000000000..20f9aa2cdd --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/utils/XmlUtils.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.shopping.xml.utils; + +public class XmlUtils { +} diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ModulesTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ModulesTest.java new file mode 100644 index 0000000000..b68c09c05a --- /dev/null +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ModulesTest.java @@ -0,0 +1,204 @@ +package com.tngtech.archunit.exampletest; + +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.JavaClasses; +import com.tngtech.archunit.core.domain.JavaPackage; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.example.AppModule; +import com.tngtech.archunit.example.ModuleApi; +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.Test; +import org.junit.experimental.categories.Category; + +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) +public class ModulesTest { + private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example"); + + /** + * 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. + */ + @Test + public void 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)) + .check(classes); + } + + /** + * 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. + */ + @Test + public void 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") + .check(classes); + } + + /** + * 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. + */ + @Test + public void 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)) + .check(classes); + } + + /** + * 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. + */ + @Test + public void 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)) + .check(classes); + } + + /** + * 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. + */ + @Test + public void 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)) + .check(classes); + } + + /** + * This example demonstrates how to check that modules only depend on each other through a specific API. + */ + @Test + public void modules_should_only_depend_on_each_other_through_module_API() { + modules() + .definedByAnnotation(AppModule.class) + .should().onlyDependOnEachOtherThroughClassesThat().areAnnotatedWith(ModuleApi.class) + .check(classes); + } + + /** + * This example demonstrates how to check for cyclic dependencies between modules. + */ + @Test + public void modules_should_be_free_of_cycles() { + modules() + .definedByAnnotation(AppModule.class) + .should().beFreeOfCycles() + .check(classes); + } + + private static DescribedPredicate>> declaredByDescriptorAnnotation() { + return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { + AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); + List allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }); + } + + private static IdentifierFromAnnotation identifierFromModulesAnnotation() { + return new IdentifierFromAnnotation(); + } + + private static DescriptorFunction> fromModulesAnnotation() { + return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), + (ArchModule.Identifier identifier, Set 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 { + 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 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 identifierInParentPackageOf(JavaPackage javaPackage) { + return () -> javaPackage.getParent() + .map(this::getIdentifierOfPackage) + .orElseGet(ArchModule.Identifier::ignore); + } + } +} diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java index e97fadfdd9..ce70ceae1d 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java @@ -5,9 +5,9 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -22,7 +22,7 @@ @Category(Example.class) public class PlantUmlArchitectureTest { - private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example.plantuml"); + private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example.shopping"); private final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml"); @Test diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java index a603cd278d..7f77edb3c3 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java @@ -5,9 +5,11 @@ import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.List; +import java.util.Set; import java.util.stream.Stream; import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaCodeUnit; @@ -23,17 +25,18 @@ import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import com.tngtech.archunit.lang.conditions.ArchPredicates; -import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; import static com.google.common.collect.Iterables.getLast; import static com.tngtech.archunit.ArchUnitArchitectureTest.THIRDPARTY_PACKAGE_IDENTIFIER; import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; import static com.tngtech.archunit.base.DescribedPredicate.anyElementThat; import static com.tngtech.archunit.base.DescribedPredicate.doNot; +import static com.tngtech.archunit.base.DescribedPredicate.equalTo; import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.ANONYMOUS_CLASSES; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; import static com.tngtech.archunit.core.domain.JavaMember.Predicates.declaredIn; @@ -51,6 +54,7 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.codeUnits; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.members; import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.util.Lists.newArrayList; public class PublicAPIRules { @@ -73,6 +77,7 @@ public class PublicAPIRules { classes() .that(haveMemberThatBelongsToPublicApi()) .should(beAnnotatedWith(PublicAPI.class).forSubtype() + .or(beAnnotatedWith(Internal.class)) .or(have(supertype(annotatedWith(PublicAPI.class)).and(not(modifier(PUBLIC)))))); @ArchTest @@ -113,13 +118,34 @@ public class PublicAPIRules { public static final ArchRule only_entry_point_and_syntax_interfaces_should_be_public = classes() .that().resideInAPackage("..syntax..") - .and().haveNameNotMatching(".*" + ArchRuleDefinition.class.getSimpleName() + ".*") + .and().haveNameNotMatching(".*RuleDefinition.*") .and().areNotInterfaces() .and().areNotAnnotatedWith(Internal.class) + .and(are(not(onlyUsedAsPublicApiParameter()))) .should().notBePublic() - .as(String.format( - "Only %s and interfaces within the ArchUnit syntax (..syntax..) should be public", - ArchRuleDefinition.class.getSimpleName())); + .as("Only RuleDefinitions and interfaces within the ArchUnit syntax (..syntax..) should be public"); + + private static DescribedPredicate onlyUsedAsPublicApiParameter() { + return DescribedPredicate.describe("only used as public API parameter", clazz -> { + Set relevantDependenciesFromPublicClasses = clazz.getDirectDependenciesToSelf().stream() + .filter(d -> d.getOriginClass().getModifiers().contains(PUBLIC)) + .filter(d -> + // this excludes fluent APIs where some public class returns a public nested class or similar + !belongTo(equalTo(d.getOriginClass())).test(d.getTargetClass()) + && !belongTo(equalTo(d.getTargetClass())).test(d.getOriginClass()) + && !d.getOriginClass().getEnclosingClass().equals(d.getTargetClass().getEnclosingClass()) + ) + .collect(toSet()); + long numberOfMethodParameterDependencies = relevantDependenciesFromPublicClasses.stream() + .map(Dependency::getOriginClass) + .distinct() + .flatMap(originClass -> originClass.getMethods().stream()) + .flatMap(method -> method.getRawParameterTypes().stream()) + .filter(parameterType -> parameterType.equals(clazz)) + .count(); + return relevantDependenciesFromPublicClasses.size() == numberOfMethodParameterDependencies; + }); + } @ArchTest public static final ArchRule parameters_of_public_API_are_public = diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java index 69f5a3602a..8fc3430ac8 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java @@ -14,6 +14,7 @@ import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Stream; import javax.annotation.Resource; @@ -23,6 +24,8 @@ import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.ArchConfiguration; import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.example.AppModule; +import com.tngtech.archunit.example.ModuleApi; import com.tngtech.archunit.example.cycles.complexcycles.slice1.ClassBeingCalledInSliceOne; import com.tngtech.archunit.example.cycles.complexcycles.slice1.ClassOfMinimalCycleCallingSliceTwo; import com.tngtech.archunit.example.cycles.complexcycles.slice1.SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree; @@ -132,12 +135,15 @@ import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; import com.tngtech.archunit.example.onionarchitecture.domain.service.ProductName; import com.tngtech.archunit.example.onionarchitecture.domain.service.ShoppingService; -import com.tngtech.archunit.example.plantuml.address.Address; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.importer.ProductImport; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.address.Address; +import com.tngtech.archunit.example.shopping.address.AddressController; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.importer.ProductImport; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; +import com.tngtech.archunit.example.shopping.xml.processor.XmlProcessor; +import com.tngtech.archunit.example.shopping.xml.types.XmlTypes; import com.tngtech.archunit.exampletest.ControllerRulesTest; import com.tngtech.archunit.exampletest.SecurityTest; import com.tngtech.archunit.testutil.TransientCopyRule; @@ -146,6 +152,7 @@ import com.tngtech.archunit.testutils.ExpectedConstructor; import com.tngtech.archunit.testutils.ExpectedField; import com.tngtech.archunit.testutils.ExpectedMethod; +import com.tngtech.archunit.testutils.ExpectedModuleDependency; import com.tngtech.archunit.testutils.ExpectedTestFailures; import com.tngtech.archunit.testutils.MessageAssertionChain; import com.tngtech.archunit.testutils.ResultStoringExtension; @@ -179,6 +186,7 @@ import static com.tngtech.archunit.testutils.ExpectedAccess.callFromMethod; import static com.tngtech.archunit.testutils.ExpectedAccess.callFromStaticInitializer; import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedClass; +import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedPackageInfo; import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedParameter; import static com.tngtech.archunit.testutils.ExpectedDependency.constructor; import static com.tngtech.archunit.testutils.ExpectedDependency.field; @@ -191,6 +199,7 @@ import static com.tngtech.archunit.testutils.ExpectedDependency.method; import static com.tngtech.archunit.testutils.ExpectedDependency.typeParameter; import static com.tngtech.archunit.testutils.ExpectedLocation.javaClass; +import static com.tngtech.archunit.testutils.ExpectedMessage.violation; import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOf; import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOfAnonymousClassOf; import static com.tngtech.archunit.testutils.ExpectedViolation.clazz; @@ -1126,6 +1135,233 @@ Stream MethodsTest() { .toDynamicTests(); } + @TestFactory + Stream ModulesTest() { + ExpectedTestFailures expectedFailures = ExpectedTestFailures + .forTests( + com.tngtech.archunit.exampletest.ModulesTest.class, + com.tngtech.archunit.exampletest.junit4.ModulesTest.class, + com.tngtech.archunit.exampletest.junit5.ModulesTest.class); + + BiConsumer expectRespectTheirDeclaredDependenciesViolations = + (moduleNames, expected) -> expected + + .by(ExpectedModuleDependency.uncontainedFrom(AddressController.class).to(AbstractController.class)) + .by(ExpectedModuleDependency.uncontainedFrom(AddressController.class).to(AbstractController.class)) + + .by(ExpectedModuleDependency.fromModule(moduleNames.address()).toModule(moduleNames.catalog()) + .including(field(Address.class, "productCatalog").ofType(ProductCatalog.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.product()).toModule(moduleNames.customer()) + .including(field(Product.class, "customer").ofType(Customer.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.product()).toModule(moduleNames.order()) + .including(method(Product.class, "getOrder").withReturnType(Order.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.customer()).toModule(moduleNames.order()) + .including(method(Customer.class, "addOrder").withParameter(Order.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.catalog()).toModule(moduleNames.order()) + .including(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class).inLine(12).asDependency()) + .including(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class).inLine(16).asDependency())) + + .by(ExpectedModuleDependency.fromModule(moduleNames.importer()).toModule(moduleNames.customer()) + .including(callFromMethod(ProductImport.class, "getCustomer") + .toConstructor(Customer.class).inLine(17).asDependency()) + .including(method(ProductImport.class, "getCustomer") + .withReturnType(Customer.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.order()).toModule(moduleNames.address()) + .including(method(Order.class, "report") + .withParameter(Address.class))); + + expectedFailures = expectedFailures + .ofRule("modules defined by packages '..shopping.(*)..' should respect their allowed dependencies " + + "{ catalog -> [product], customer -> [address], importer -> [catalog, xml], order -> [customer, product] } " + + "considering only dependencies in any package ['..example..']"); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByPackages(), expectedFailures); + + Consumer expectDependOnEachOtherThroughViolations = + expected -> expected + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .by(field(ProductImport.class, "productCatalog").ofType(ProductCatalog.class)) + .by(field(ProductImport.class, "xmlType").ofType(XmlTypes.class)) + .by(callFromMethod(ProductImport.class, "parse", byte[].class).toConstructor(ProductCatalog.class).inLine(21).asDependency()) + .by(method(ProductImport.class, "parse").withReturnType(ProductCatalog.class)); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by annotation @%s should respect their allowed dependencies declared in 'allowedDependencies' " + + "considering only dependencies in any package ['..example..'] " + + "and should only depend on each other through packages declared in 'exposedPackages'", + AppModule.class.getSimpleName())); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + expectDependOnEachOtherThroughViolations.accept(expectedFailures); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by annotation @%s should respect their allowed dependencies declared by descriptor annotation" + + " considering only dependencies in any package ['..example..']", + AppModule.class.getSimpleName())); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by root classes annotated with @%s ", AppModule.class.getSimpleName()) + + String.format("deriving module from root class by annotation @%s ", AppModule.class.getSimpleName()) + + "should respect their allowed dependencies declared by descriptor annotation considering only dependencies in any package ['..example..']"); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by root classes with annotation @%s ", AppModule.class.getSimpleName()) + + String.format("deriving module from @%s(name) ", AppModule.class.getSimpleName()) + + "should respect their allowed dependencies declared by descriptor annotation considering only dependencies in any package ['..example..']"); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + + expectedFailures + .ofRule("modules defined by annotation @AppModule should only depend on each other through classes that are annotated with @ModuleApi"); + expectDependOnEachOtherThroughViolations.accept(expectedFailures); + + expectedFailures.ofRule("modules defined by annotation @AppModule should be free of cycles") + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class) + .inLine(12)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class) + .inLine(16)) + .from("Order") + .by(method(Order.class, "report").withParameter(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class) + .inLine(12)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class) + .inLine(16)) + .from("Order") + .by(field(Order.class, "customer").ofType(Customer.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Customer.class, "getAddress") + .inLine(21)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class) + .inLine(12)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class) + .inLine(16)) + .from("Order") + .by(genericFieldType(Order.class, "products").dependingOn(Product.class)) + .by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Product.class, "report") + .inLine(23)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class)) + .from("Customer") + .by(method(Customer.class, "addOrder").withParameter(Order.class)) + .from("Order") + .by(method(Order.class, "report").withParameter(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(method(Product.class, "getOrder").withReturnType(Order.class)) + .from("Order") + .by(method(Order.class, "report").withParameter(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(method(Product.class, "getOrder").withReturnType(Order.class)) + .from("Order") + .by(field(Order.class, "customer").ofType(Customer.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Customer.class, "getAddress") + .inLine(21)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Customer") + .by(method(Customer.class, "addOrder").withParameter(Order.class)) + .from("Order") + .by(field(Order.class, "customer").ofType(Customer.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Customer.class, "getAddress") + .inLine(21))) + .by(cycle() + .from("Customer") + .by(method(Customer.class, "addOrder").withParameter(Order.class)) + .from("Order") + .by(genericFieldType(Order.class, "products").dependingOn(Product.class)) + .by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Product.class, "report") + .inLine(23)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class))) + .by(cycle() + .from("Order") + .by(genericFieldType(Order.class, "products").dependingOn(Product.class)) + .by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Product.class, "report") + .inLine(23)) + .from("Product") + .by(method(Product.class, "getOrder").withReturnType(Order.class))); + + return expectedFailures.toDynamicTests(); + } + @TestFactory Stream NamingConventionTest() { return ExpectedTestFailures @@ -1199,7 +1435,7 @@ Stream PlantUmlArchitectureTest() { .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") .toMethod(Order.class, "addProducts", Set.class).inLine(16).asDependency()) .by(callFromMethod(ProductImport.class, "getCustomer") - .toConstructor(Customer.class).inLine(14).asDependency()) + .toConstructor(Customer.class).inLine(17).asDependency()) .by(method(ProductImport.class, "getCustomer") .withReturnType(Customer.class)) .by(method(Order.class, "report") @@ -1212,16 +1448,35 @@ Stream PlantUmlArchitectureTest() { ProductCatalog.class.getName(), Product.class.getName(), Order.class.getName())) .by(field(Address.class, "productCatalog") .ofType(ProductCatalog.class)) + .by(inheritanceFrom(AddressController.class) + .extending(AbstractController.class)) + .by(callFromConstructor(AddressController.class) + .toConstructor(AbstractController.class) + .inLine(8).asDependency()) .by(field(Product.class, "customer") .ofType(Customer.class)) .by(method(Customer.class, "addOrder") .withParameter(Order.class)) .by(callFromMethod(ProductImport.class, "getCustomer") - .toConstructor(Customer.class).inLine(14).asDependency()) + .toConstructor(Customer.class).inLine(17).asDependency()) .by(method(ProductImport.class, "getCustomer") .withReturnType(Customer.class)) .by(method(Order.class, "report") .withParameter(Address.class)) + .by(annotatedClass(Address.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(AddressController.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(Customer.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(ProductImport.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(Order.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(Product.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(XmlProcessor.class).annotatedWith(ModuleApi.class)) + .by(annotatedPackageInfo(Address.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(ProductCatalog.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(Customer.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(ProductImport.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(Order.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(Product.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(violation("Class com.tngtech.archunit.example.shopping.xml.package-info is not contained in any component")) .toDynamicTests(); } @@ -1498,4 +1753,44 @@ Stream ThirdPartyRulesTest() { .toDynamicTests(); } + + private static class ModuleNames { + private final Function nameModification; + + private ModuleNames(Function nameModification) { + this.nameModification = nameModification; + } + + String address() { + return nameModification.apply("address"); + } + + String catalog() { + return nameModification.apply("catalog"); + } + + String customer() { + return nameModification.apply("customer"); + } + + String order() { + return nameModification.apply("order"); + } + + String product() { + return nameModification.apply("product"); + } + + String importer() { + return nameModification.apply("importer"); + } + + static ModuleNames definedByPackages() { + return new ModuleNames(Function.identity()); + } + + static ModuleNames definedByMetaInfo() { + return new ModuleNames(name -> name.substring(0, 1).toUpperCase() + name.substring(1)); + } + } } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java index 8816e00edc..29688a1ae8 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java @@ -40,14 +40,14 @@ private String detailText() { private List detailLines() { List result = new ArrayList<>(); for (Map.Entry> detail : details.asMap().entrySet()) { - result.add(dependenciesOfSliceHeaderPattern(detail.getKey())); + result.add(dependenciesOfComponentHeaderPattern(detail.getKey())); result.addAll(transform(detail.getValue(), r -> detailLinePattern(r.toString()))); } return result; } - public CyclicErrorMatcher from(String sliceName) { - cycleDescriptions.add(sliceName); + public CyclicErrorMatcher from(String componentName) { + cycleDescriptions.add(componentName); return this; } @@ -61,8 +61,8 @@ public MessageAssertionChain.Link.Result filterMatching(List lines) { Result.Builder builder = new Result.Builder() .containsText(cycleText()); - for (String sliceName : details.asMap().keySet()) { - builder.matchesLine(dependenciesOfSliceHeaderPattern(sliceName)); + for (String componentName : details.asMap().keySet()) { + builder.matchesLine(dependenciesOfComponentHeaderPattern(componentName)); } for (ExpectedRelation relation : details.values()) { @@ -82,8 +82,8 @@ public void associateIfStringIsContained(String string) { return builder.build(lines); } - private String dependenciesOfSliceHeaderPattern(String sliceName) { - return "\\s*\\d+. Dependencies of " + quote(sliceName); + private String dependenciesOfComponentHeaderPattern(String componentName) { + return "\\s*\\d+. Dependencies of " + quote(componentName); } private String detailLinePattern(String string) { diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java index ba7e57cd22..10c7e27951 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java @@ -82,7 +82,7 @@ public boolean correspondsTo(Object object) { public abstract ExpectedDependency asDependency(); public static class ExpectedAccessViolationCreationProcess { - private ExpectedOrigin origin; + private final ExpectedOrigin origin; private ExpectedAccessViolationCreationProcess(String memberDescription, Class clazz, String method, Class[] paramTypes) { origin = new ExpectedOrigin(memberDescription, clazz, method, paramTypes); @@ -162,6 +162,11 @@ public ExpectedDependency asDependency() { .toFieldDeclaredIn(getTarget().getDeclaringClass()) .inLineNumber(getLineNumber()); } + + @Override + public void addTo(HandlingAssertion assertion) { + assertion.byFieldAccess(this); + } } public static class ExpectedCall extends ExpectedAccess { @@ -179,5 +184,14 @@ public ExpectedDependency asDependency() { .toCodeUnitDeclaredIn(getTarget().getDeclaringClass()) .inLineNumber(getLineNumber()); } + + @Override + public void addTo(HandlingAssertion assertion) { + if (isToConstructor()) { + assertion.byConstructorCall(this); + } else { + assertion.byMethodCall(this); + } + } } } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java index 13172db16c..b7b36f505f 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java @@ -19,7 +19,7 @@ public class ExpectedDependency implements ExpectedRelation { private final Class origin; private final Class target; - private String dependencyPattern; + private final String dependencyPattern; private ExpectedDependency(Class origin, Class target, String dependencyPattern) { this.origin = origin; @@ -80,6 +80,14 @@ public static GenericMemberTypeArgumentCreator genericMethodParameterType(Class< genericParameterTypes[0]); } + public static AnnotationDependencyCreator annotatedPackageInfo(String packageName) { + try { + return new AnnotationDependencyCreator(Class.forName(packageName + ".package-info")); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + public static AnnotationDependencyCreator annotatedClass(Class clazz) { return new AnnotationDependencyCreator(clazz); } @@ -104,6 +112,11 @@ public static MemberDependencyCreator constructor(Class owner) { return new MemberDependencyCreator(owner, CONSTRUCTOR_NAME); } + @Override + public void addTo(HandlingAssertion assertion) { + assertion.byDependency(this); + } + @Override public void associateLines(LineAssociation association) { association.associateIfPatternMatches(dependencyPattern); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java index 1f9a6a2418..560b65a2fe 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java @@ -4,7 +4,7 @@ import static java.util.stream.Collectors.toList; -class ExpectedMessage implements MessageAssertionChain.Link { +public class ExpectedMessage implements MessageAssertionChain.Link { private final String expectedMessage; ExpectedMessage(String expectedMessage) { @@ -22,4 +22,8 @@ public Result filterMatching(List lines) { public String getDescription() { return "Message: " + expectedMessage; } + + public static ExpectedMessage violation(String message) { + return new ExpectedMessage(message); + } } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedModuleDependency.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedModuleDependency.java new file mode 100644 index 0000000000..daaeb86b1b --- /dev/null +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedModuleDependency.java @@ -0,0 +1,108 @@ +package com.tngtech.archunit.testutils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.base.Joiner; +import com.tngtech.archunit.core.domain.Dependency; + +import static com.tngtech.archunit.testutils.MessageAssertionChain.matchesLine; +import static java.util.regex.Pattern.quote; + +public class ExpectedModuleDependency implements MessageAssertionChain.Link { + private final String dependencyPattern; + private final Set details = new HashSet<>(); + + private ExpectedModuleDependency(String dependencyPattern) { + this.dependencyPattern = dependencyPattern; + } + + public static ModuleDependencyCreator fromModule(String moduleName) { + return new ModuleDependencyCreator(moduleName); + } + + public static UncontainedCreator uncontainedFrom(Class origin) { + return new UncontainedCreator(origin); + } + + @Override + public Result filterMatching(List lines) { + return new Result.Builder() + .matchesLine(dependencyPattern) + .contains(details) + .build(lines); + } + + @Override + public String getDescription() { + return String.format("Module Dependency :: matches %s :: matches each of [%s]", + dependencyPattern, Joiner.on(", ").join(details)); + } + + public ExpectedModuleDependency including(ExpectedRelation relation) { + details.add(relation); + return this; + } + + public static class ModuleDependencyCreator { + private final String originModuleName; + + private ModuleDependencyCreator(String originModuleName) { + this.originModuleName = originModuleName; + } + + public ExpectedModuleDependency toModule(String moduleName) { + String description = quote(String.format("[%s -> %s]", originModuleName, moduleName)); + return new ExpectedModuleDependency(String.format("Module Dependency %s.*", description)); + } + } + + public static class UncontainedCreator { + private final Class origin; + + private UncontainedCreator(Class origin) { + this.origin = origin; + } + + public ExpectedUncontainedModuleDependency to(Class target) { + return new ExpectedUncontainedModuleDependency(origin, target); + } + } + + private static class ExpectedUncontainedModuleDependency implements MessageAssertionChain.Link, ExpectedRelation { + private final String dependencyPattern; + private final MessageAssertionChain.Link delegate; + + private ExpectedUncontainedModuleDependency(Class origin, Class target) { + dependencyPattern = String.format("Dependency not contained in any module: .*%s.*%s.*", + quote(origin.getName()), quote(target.getName())); + this.delegate = matchesLine(dependencyPattern); + } + + @Override + public void addTo(HandlingAssertion assertion) { + assertion.byDependency(this); + } + + @Override + public void associateLines(LineAssociation association) { + association.associateIfPatternMatches(dependencyPattern); + } + + @Override + public boolean correspondsTo(Object object) { + return object instanceof Dependency; + } + + @Override + public Result filterMatching(List lines) { + return delegate.filterMatching(lines); + } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + } +} diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java index 614680c93c..7cd79aff37 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java @@ -3,12 +3,15 @@ import com.tngtech.archunit.core.domain.JavaAccess; import com.tngtech.archunit.lang.ConditionEvent; -public interface ExpectedRelation { +interface ExpectedRelation { + + void addTo(HandlingAssertion assertion); + void associateLines(LineAssociation association); /** * @return True, if this expected dependency refers to the supplied object - * (i.e. the object that was passed to the {@link ConditionEvent}, e.g. a {@link JavaAccess}) + * (i.e. the object that was passed to the {@link ConditionEvent}, e.g. a {@link JavaAccess}) */ boolean correspondsTo(Object object); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java index 6f337dc86b..ed60b145d2 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java @@ -341,21 +341,24 @@ boolean isAssignedTo(TestFailure failure) { void by(ExpectedAccess.ExpectedFieldAccess access) { expectedViolation.by(access); - handlingAssertion.by(access); + access.addTo(handlingAssertion); } void by(ExpectedAccess.ExpectedCall call) { expectedViolation.by(call); - handlingAssertion.by(call); + call.addTo(handlingAssertion); } void by(ExpectedDependency inheritance) { expectedViolation.by(inheritance); - handlingAssertion.by(inheritance); + inheritance.addTo(handlingAssertion); } void by(MessageAssertionChain.Link assertion) { expectedViolation.by(assertion); + if (assertion instanceof ExpectedRelation) { + ((ExpectedRelation) assertion).addTo(handlingAssertion); + } } ExpectedViolationToAssign copy() { diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java index dd7795c9c8..ebe481002a 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java @@ -15,8 +15,6 @@ import com.tngtech.archunit.core.domain.JavaFieldAccess; import com.tngtech.archunit.core.domain.JavaMethodCall; import com.tngtech.archunit.lang.EvaluationResult; -import com.tngtech.archunit.testutils.ExpectedAccess.ExpectedCall; -import com.tngtech.archunit.testutils.ExpectedAccess.ExpectedFieldAccess; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Sets.union; @@ -47,19 +45,19 @@ private HandlingAssertion( this.expectedDependencies = expectedDependencies; } - void by(ExpectedFieldAccess access) { + void byFieldAccess(ExpectedRelation access) { expectedFieldAccesses.add(access); } - void by(ExpectedCall call) { - if (call.isToConstructor()) { - expectedConstructorCalls.add(call); - } else { - expectedMethodCalls.add(call); - } + void byConstructorCall(ExpectedRelation call) { + expectedConstructorCalls.add(call); + } + + void byMethodCall(ExpectedRelation call) { + expectedMethodCalls.add(call); } - void by(ExpectedDependency inheritance) { + void byDependency(ExpectedRelation inheritance) { expectedDependencies.add(inheritance); } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java index cd470b14c6..f9c423e09c 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java @@ -11,10 +11,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.tngtech.archunit.Internal; +import com.tngtech.archunit.testutils.ExpectedRelation.LineAssociation; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.System.lineSeparator; -import static java.util.Collections.singletonList; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -217,16 +217,6 @@ public Result(boolean matches, List remainingLines, String mismatchDescr this.mismatchDescription = mismatchDescription; } - static List difference(List list, String toSubtract) { - return difference(list, singletonList(toSubtract)); - } - - static List difference(List list, List toSubtract) { - List result = new ArrayList<>(list); - result.removeAll(toSubtract); - return result; - } - Optional getMismatchDescription() { return Optional.ofNullable(mismatchDescription); } @@ -250,6 +240,23 @@ Builder matchesLine(String pattern) { return this; } + Builder contains(Iterable relations) { + for (ExpectedRelation relation : relations) { + relation.associateLines(new LineAssociation() { + @Override + public void associateIfPatternMatches(String pattern) { + matchesLine(pattern); + } + + @Override + public void associateIfStringIsContained(String string) { + containsLine(string); + } + }); + } + return this; + } + Result build(List lines) { boolean matches = true; List remainingLines = new ArrayList<>(lines); diff --git a/archunit/src/main/java/com/tngtech/archunit/base/DescribedFunction.java b/archunit/src/main/java/com/tngtech/archunit/base/DescribedFunction.java new file mode 100644 index 0000000000..08dbd125e1 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/base/DescribedFunction.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.base; + +import java.util.function.Function; + +import com.tngtech.archunit.PublicAPI; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; + +@PublicAPI(usage = INHERITANCE) +public abstract class DescribedFunction implements Function, HasDescription { + private final String description; + + protected DescribedFunction(String description, Object... args) { + this.description = String.format(description, args); + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("description", description) + .toString(); + } + + @PublicAPI(usage = ACCESS) + public static DescribedFunction describe(String description, final Function function) { + return new DescribedFunction(description) { + @Override + public T apply(F input) { + return function.apply(input); + } + }; + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java index a16102f5c0..2993a8abf8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java @@ -244,11 +244,17 @@ private static String bracketFormat(String name) { return "<" + name + ">"; } + /** + * @return The class where this dependency originates from (e.g. because the origin class calls a method of another class) + */ @PublicAPI(usage = ACCESS) public JavaClass getOriginClass() { return originClass; } + /** + * @return The class that is targeted by this dependency (e.g. because it contains a method that is called from another class) + */ @PublicAPI(usage = ACCESS) public JavaClass getTargetClass() { return targetClass; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/AbstractGivenObjects.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/AbstractGivenObjects.java index 594cc7b705..2e8ab6091a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/AbstractGivenObjects.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/AbstractGivenObjects.java @@ -54,9 +54,7 @@ SELF with(PredicateAggregator newPredicate) { } ClassesTransformer finishedClassesTransformer() { - ClassesTransformer completeTransformation = relevantObjectsPredicates.isPresent() ? - classesTransformer.that(relevantObjectsPredicates.get()) : - classesTransformer; + ClassesTransformer completeTransformation = relevantObjectsPredicates.map(classesTransformer::that).orElse(classesTransformer); return overriddenDescription.isPresent() ? completeTransformation.as(overriddenDescription.get()) : completeTransformation; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesThatInternal.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesThatInternal.java index 67487baad2..ffc55ce7a7 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesThatInternal.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/ClassesThatInternal.java @@ -18,6 +18,7 @@ import java.lang.annotation.Annotation; import java.util.function.Function; +import com.tngtech.archunit.Internal; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; @@ -53,10 +54,11 @@ import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; -class ClassesThatInternal implements ClassesThat { +@Internal +public final class ClassesThatInternal implements ClassesThat { private final Function, CONJUNCTION> addPredicate; - ClassesThatInternal(Function, CONJUNCTION> addPredicate) { + public ClassesThatInternal(Function, CONJUNCTION> addPredicate) { this.addPredicate = checkNotNull(addPredicate); } @@ -450,10 +452,6 @@ public CONJUNCTION doNotHaveModifier(JavaModifier modifier) { return givenWith(SyntaxPredicates.doNotHaveModifier(modifier)); } - private CONJUNCTION givenWith(DescribedPredicate predicate) { - return addPredicate.apply(predicate); - } - @Override public CONJUNCTION containAnyMembersThat(DescribedPredicate predicate) { return givenWith(JavaClass.Predicates.containAnyMembersThat(predicate)); @@ -483,4 +481,8 @@ public CONJUNCTION containAnyConstructorsThat(DescribedPredicate predicate) { return givenWith(JavaClass.Predicates.containAnyStaticInitializersThat(predicate)); } + + private CONJUNCTION givenWith(DescribedPredicate predicate) { + return addPredicate.apply(predicate); + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/PredicateAggregator.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/PredicateAggregator.java index a5ba39aa9b..d961291ce3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/PredicateAggregator.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/PredicateAggregator.java @@ -16,12 +16,14 @@ package com.tngtech.archunit.lang.syntax; import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; import com.tngtech.archunit.Internal; import com.tngtech.archunit.base.DescribedPredicate; @Internal -public final class PredicateAggregator { +public final class PredicateAggregator implements Predicate { private final AddMode addMode; private final Optional> predicate; @@ -34,6 +36,11 @@ private PredicateAggregator(AddMode addMode, Optional> this.predicate = predicate; } + @Override + public boolean test(T t) { + return predicate.map(it -> it.test(t)).orElse(true); + } + public PredicateAggregator add(DescribedPredicate other) { return new PredicateAggregator<>(addMode, Optional.of(addMode.apply(predicate, other))); } @@ -42,6 +49,10 @@ public boolean isPresent() { return predicate.isPresent(); } + public Optional map(Function, V> function) { + return predicate.map(function); + } + public DescribedPredicate get() { return predicate.get(); } diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/GivenClasses.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/GivenClasses.java index 0447b1b768..5443cf8f2e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/GivenClasses.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/GivenClasses.java @@ -63,10 +63,10 @@ public interface GivenClasses extends GivenObjects { * * {@link ArchRuleDefinition#classes() classes()}.{@link GivenClasses#should() should()}.{@link ClassesShould#haveSimpleName(String) haveSimpleName("Example")} * - * + *
* Use {@link #should(ArchCondition)} to freely customize the condition against which the classes should be checked. * - * @return A syntax element, which can be used to restrict the classes under consideration + * @return A syntax element, which can be used to create rules for the classes under consideration */ @PublicAPI(usage = ACCESS) ClassesShould should(); diff --git a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java index d8d3a0bb97..01dd3e9659 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java @@ -338,9 +338,7 @@ private DescribedPredicate targetMatchesIfDependencyIsRelevant(Strin private DescribedPredicate ifDependencyIsRelevant(DescribedPredicate predicate) { DescribedPredicate configuredPredicate = dependencySettings.ignoreExcludedDependencies.apply(layerDefinitions, predicate); - return irrelevantDependenciesPredicate.isPresent() ? - configuredPredicate.or(irrelevantDependenciesPredicate.get()) : - configuredPredicate; + return irrelevantDependenciesPredicate.map(configuredPredicate::or).orElse(configuredPredicate); } @Override diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Cycle.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Cycle.java new file mode 100644 index 0000000000..9d283cd628 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Cycle.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection; + +import java.util.List; + +import com.tngtech.archunit.PublicAPI; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +/** + * A cycle formed by the referenced {@code EDGEs}. A cycle in this context always refers to a "simple" cycle, + * i.e. the list of edges is not empty, the {@link Edge#getOrigin() origin} of the first {@link Edge} is equal + * to the {@link Edge#getTarget() target} of the last {@link Edge} and every node contained in the cycle + * is contained exactly once. + * + * @param The type of the edges forming the cycle + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface Cycle> { + + /** + * @return The edges of the {@link Cycle} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + List getEdges(); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleConfiguration.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleConfiguration.java new file mode 100644 index 0000000000..24085e75df --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection; + +import com.tngtech.archunit.ArchConfiguration; +import com.tngtech.archunit.Internal; + +@Internal +public final class CycleConfiguration { + /** + * Configures the maximum number of cycles to detect by {@link CycleDetector}. I.e. once this number + * of cycles has been found the algorithm will stop its search and report the cycles found so far. + */ + @Internal + public static final String MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME = "cycles.maxNumberToDetect"; + private static final String MAX_NUMBER_OF_CYCLES_TO_DETECT_DEFAULT_VALUE = "100"; + + private final int maxCyclesToDetect; + + CycleConfiguration() { + String configuredMaxCyclesToDetect = ArchConfiguration.get() + .getPropertyOrDefault(MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME, MAX_NUMBER_OF_CYCLES_TO_DETECT_DEFAULT_VALUE); + maxCyclesToDetect = Integer.parseInt(configuredMaxCyclesToDetect); + } + + int getMaxNumberOfCyclesToDetect() { + return maxCyclesToDetect; + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleDetector.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleDetector.java new file mode 100644 index 0000000000..6b07212fa5 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleDetector.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection; + +import java.util.Collection; + +import com.tngtech.archunit.PublicAPI; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +/** + * @see #detectCycles(Collection, Collection) + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class CycleDetector { + private CycleDetector() { + } + + /** + * Detects cycles in directed graphs consisting of nodes of type {@code NODE} which are connected + * by directed edges of type {@code EDGE}.
+ * All reported cycles are "simple" cycles, i.e. they pass each node at most once. + * The algorithm reports all such simple cycles, even if nodes / edges are already part of another cycle. + * Consider the following case: + *

+ * + *

+ * Then both cycles, red and blue, would be reported.

+ * For performance reasons the {@link CycleDetector} has a maximum number of cycles to detect. + * Once this limit is reached the algorithm will terminate and all cycles so far will be reported. + * For further information please refer to {@link Cycles#maxNumberOfCyclesReached()}.

+ * Note that the given edges must only reference the given nodes as their {@link Edge#getOrigin() origin} and + * {@link Edge#getTarget() target} or an exception will be thrown.
+ * Also, the passed {@code NODE} types must implement an appropriate and reasonably performant {@link Object#equals(Object)} + * and {@link Object#hashCode()}. + * + * @param nodes The nodes of the graph to create + * @param edges The edges connecting the nodes of the graph + * @return All cycles within the graph created from the passed nodes and edges. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static > Cycles detectCycles(Collection nodes, Collection edges) { + Graph graph = new Graph<>(); + graph.addNodes(nodes); + graph.addEdges(edges); + return graph.findCycles(); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Edge.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleInternal.java similarity index 50% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/Edge.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleInternal.java index 752676f566..b7b5da3aac 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Edge.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/CycleInternal.java @@ -13,42 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; -import java.util.Collection; import java.util.List; import java.util.Objects; -import com.google.common.collect.ImmutableList; +class CycleInternal> implements Cycle { + private final Path path; -final class Edge { - private final T from; - private final T to; - private final List attachments; - private final int hashCode; - - Edge(T from, T to, Collection attachments) { - this.from = from; - this.to = to; - hashCode = Objects.hash(from, to); - this.attachments = ImmutableList.copyOf(attachments); + CycleInternal(List edges) { + this(new Path<>(edges)); } - T getFrom() { - return from; + CycleInternal(Path path) { + if (!path.formsCycle()) { + throwNoCycleException(path); + } + this.path = path; } - T getTo() { - return to; + private void throwNoCycleException(Path path) { + throw new IllegalArgumentException("The supplied edges do not form a cycle. Edges were " + path); } - List getAttachments() { - return attachments; + @Override + public List getEdges() { + return path.getEdges(); } @Override public int hashCode() { - return hashCode; + return Objects.hash(path.getEdges()); } @Override @@ -59,17 +54,12 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - Edge other = (Edge) obj; - return Objects.equals(this.from, other.from) - && Objects.equals(this.to, other.to); + CycleInternal other = (CycleInternal) obj; + return Objects.equals(this.path.getEdges(), other.path.getEdges()); } @Override public String toString() { - return "Edge{" + - "from=" + from + - ", to=" + to + - ", attachments=" + attachments + - '}'; + return "Cycle{" + path.edgesToString() + '}'; } } diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Cycles.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Cycles.java new file mode 100644 index 0000000000..c51ca759de --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Cycles.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection; + +import java.util.Collection; + +import com.tngtech.archunit.PublicAPI; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface Cycles> extends Collection> { + + /** + * @return {@code true}, if the maximum number of cycles to detect had been reached. + * I.e. if {@code true} there could be more cycles in the examined graph that are omitted from the result, + * if {@code false} then all the cycles of the graph are reported.

+ * The maximum number of cycles at which the algorithm will stop can be configured by the {@code archunit.properties} + * property {@value CycleConfiguration#MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + boolean maxNumberOfCyclesReached(); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Edge.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Edge.java new file mode 100644 index 0000000000..72d5aa112c --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Edge.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection; + +import com.tngtech.archunit.PublicAPI; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; + +@PublicAPI(usage = INHERITANCE) +public interface Edge { + + NODE getOrigin(); + + NODE getTarget(); + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + static Edge create(N origin, N target) { + return new SimpleEdge<>(origin, target); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Graph.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Graph.java new file mode 100644 index 0000000000..bd4669fd06 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Graph.java @@ -0,0 +1,134 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import com.tngtech.archunit.base.ForwardingCollection; + +import static com.google.common.base.Preconditions.checkArgument; + +class Graph> { + private final Map nodes = new HashMap<>(); + private final ListMultimap outgoingEdges = ArrayListMultimap.create(); + + void addNodes(Collection nodes) { + for (NODE node : nodes) { + if (!this.nodes.containsKey(node)) { + this.nodes.put(node, this.nodes.size()); + } + } + } + + void addEdges(Iterable outgoingEdges) { + for (EDGE edge : outgoingEdges) { + checkArgument(nodes.containsKey(edge.getOrigin()), "Node %s of edge %s is not part of the graph", edge.getOrigin(), edge); + checkArgument(nodes.containsKey(edge.getTarget()), "Node %s of edge %s is not part of the graph", edge.getTarget(), edge); + this.outgoingEdges.put(nodes.get(edge.getOrigin()), edge); + } + } + + Cycles findCycles() { + JohnsonCycleFinder johnsonCycleFinder = new JohnsonCycleFinder(createPrimitiveGraph()); + JohnsonCycleFinder.Result rawCycles = johnsonCycleFinder.findCycles(); + return new CyclesInternal<>(mapToCycles(rawCycles), rawCycles.maxNumberOfCyclesReached()); + } + + private PrimitiveGraph createPrimitiveGraph() { + int[][] edges = new int[nodes.size()][]; + for (Map.Entry nodeToIndex : nodes.entrySet()) { + List outgoing = outgoingEdges.get(nodeToIndex.getValue()); + edges[nodeToIndex.getValue()] = new int[outgoing.size()]; + for (int j = 0; j < outgoing.size(); j++) { + edges[nodeToIndex.getValue()][j] = nodes.get(outgoing.get(j).getTarget()); + } + } + return new PrimitiveGraph(edges); + } + + private ImmutableList> mapToCycles(JohnsonCycleFinder.Result rawCycles) { + Map> edgesByTargetIndexByOriginIndex = indexEdgesByTargetIndexByOriginIndex(nodes, outgoingEdges); + ImmutableList.Builder> result = ImmutableList.builder(); + for (int[] rawCycle : rawCycles) { + result.add(mapToCycle(edgesByTargetIndexByOriginIndex, rawCycle)); + } + return result.build(); + } + + private ImmutableMap> indexEdgesByTargetIndexByOriginIndex( + Map nodes, + Multimap outgoingEdges) { + + ImmutableMap.Builder> edgeMapBuilder = ImmutableMap.builder(); + for (Map.Entry> originIndexToEdges : outgoingEdges.asMap().entrySet()) { + ImmutableMap.Builder targetIndexToEdges = ImmutableMap.builder(); + for (EDGE edge : originIndexToEdges.getValue()) { + targetIndexToEdges.put(nodes.get(edge.getTarget()), edge); + } + edgeMapBuilder.put(originIndexToEdges.getKey(), targetIndexToEdges.build()); + } + return edgeMapBuilder.build(); + } + + private Cycle mapToCycle(Map> edgesByTargetIndexByOriginIndex, int[] rawCycle) { + ImmutableList.Builder edges = ImmutableList.builder(); + int originIndex = -1; + for (int targetIndex : rawCycle) { + if (originIndex >= 0) { + edges.add(edgesByTargetIndexByOriginIndex.get(originIndex).get(targetIndex)); + } + originIndex = targetIndex; + } + edges.add(edgesByTargetIndexByOriginIndex.get(originIndex).get(rawCycle[0])); + return new CycleInternal<>(edges.build()); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "nodes=" + nodes + + ", edges=" + outgoingEdges + + '}'; + } + + private static class CyclesInternal> extends ForwardingCollection> implements Cycles { + private final Collection> cycles; + private final boolean maxNumberOfCyclesReached; + + private CyclesInternal(Collection> cycles, boolean maxNumberOfCyclesReached) { + this.cycles = cycles; + this.maxNumberOfCyclesReached = maxNumberOfCyclesReached; + } + + @Override + public boolean maxNumberOfCyclesReached() { + return maxNumberOfCyclesReached; + } + + @Override + protected Collection> delegate() { + return cycles; + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/JohnsonComponent.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/JohnsonComponent.java similarity index 97% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/JohnsonComponent.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/JohnsonComponent.java index 62e1898a23..f8857620d0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/JohnsonComponent.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/JohnsonComponent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; import java.util.Arrays; import java.util.HashSet; @@ -21,7 +21,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import com.tngtech.archunit.library.dependencies.PrimitiveDataTypes.IntStack; +import com.tngtech.archunit.library.cycle_detection.PrimitiveDataTypes.IntStack; import static java.util.Arrays.binarySearch; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/JohnsonCycleFinder.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/JohnsonCycleFinder.java similarity index 95% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/JohnsonCycleFinder.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/JohnsonCycleFinder.java index 6bfb6e8db1..438460b24d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/JohnsonCycleFinder.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/JohnsonCycleFinder.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; import java.util.ArrayList; import java.util.Iterator; @@ -22,8 +22,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.tngtech.archunit.library.dependencies.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; -import static com.tngtech.archunit.library.dependencies.TarjanComponentFinder.NO_COMPONENT_FOUND; +import static com.tngtech.archunit.library.cycle_detection.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; +import static com.tngtech.archunit.library.cycle_detection.TarjanComponentFinder.NO_COMPONENT_FOUND; /** * An implementation of Johnson's algorithm to find cycles within an uni-directed graph diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Path.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Path.java similarity index 57% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/Path.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Path.java index 6a51436c8f..4c129e15ba 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Path.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/Path.java @@ -13,32 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; import java.util.ArrayList; import java.util.List; -import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Set; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import static com.google.common.collect.Iterables.getLast; import static java.util.Collections.emptyList; -class Path { - private final List> edges; +class Path> { + private final List edges; Path() { this(emptyList()); } - Path(Path other) { + Path(Path other) { this(other.getEdges()); } - Path(List> edges) { + Path(List edges) { this.edges = new ArrayList<>(edges); validateEdgesConnect(); } @@ -47,55 +44,35 @@ private void validateEdgesConnect() { if (edges.isEmpty()) { return; } - Object expectedFrom = edges.get(0).getFrom(); - for (Edge edge : edges) { + Object expectedFrom = edges.get(0).getOrigin(); + for (EDGE edge : edges) { verifyEdgeFromMatches(expectedFrom, edge); - expectedFrom = edge.getTo(); + expectedFrom = edge.getTarget(); } } - private void verifyEdgeFromMatches(Object expectedFrom, Edge edge) { - if (!expectedFrom.equals(edge.getFrom())) { + private void verifyEdgeFromMatches(Object expectedFrom, EDGE edge) { + if (!expectedFrom.equals(edge.getOrigin())) { throw new IllegalArgumentException("Edges are not connected: " + edges); } } - List> getEdges() { + List getEdges() { return ImmutableList.copyOf(edges); } - Set> getSetOfEdges() { - return ImmutableSet.copyOf(edges); - } - - Path append(Edge edge) { - if (!edges.isEmpty()) { - verifyEdgeFromMatches(getLast(edges).getTo(), edge); - } - edges.add(edge); - return this; - } - boolean isEmpty() { return edges.isEmpty(); } - T getStart() { - if (edges.isEmpty()) { - throw new NoSuchElementException("Empty path has no start"); - } - return edges.get(0).getFrom(); - } - - T getEnd() { - if (edges.isEmpty()) { - throw new NoSuchElementException("Empty path has no end"); + boolean formsCycle() { + if (isEmpty()) { + return false; } - return edges.get(edges.size() - 1).getTo(); - } - public boolean isCycle() { - return !isEmpty() && getStart().equals(getEnd()); + Object start = edges.get(0).getOrigin(); + Object end = getLast(edges).getTarget(); + return start.equals(end); } @Override @@ -111,7 +88,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - Path other = (Path) obj; + Path other = (Path) obj; return Objects.equals(this.edges, other.edges); } diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/PrimitiveDataTypes.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/PrimitiveDataTypes.java similarity index 97% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/PrimitiveDataTypes.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/PrimitiveDataTypes.java index d8febf086c..362cfb5056 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/PrimitiveDataTypes.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/PrimitiveDataTypes.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; import static com.google.common.base.Preconditions.checkState; import static java.util.Arrays.copyOf; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/PrimitiveGraph.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/PrimitiveGraph.java similarity index 96% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/PrimitiveGraph.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/PrimitiveGraph.java index 3fe464e725..5b9b1b4ef0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/PrimitiveGraph.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/PrimitiveGraph.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; /** * An optimized graph stripped down to the bare minimum for cycle detection. diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/SimpleEdge.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/SimpleEdge.java new file mode 100644 index 0000000000..f93a9d0a8c --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/SimpleEdge.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +class SimpleEdge implements Edge { + private final NODE origin; + private final NODE target; + + SimpleEdge(NODE origin, NODE target) { + this.origin = checkNotNull(origin); + this.target = checkNotNull(target); + } + + @Override + public NODE getOrigin() { + return origin; + } + + @Override + public NODE getTarget() { + return target; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleEdge that = (SimpleEdge) o; + return origin.equals(that.origin) && target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(origin, target); + } + + @Override + public String toString() { + return toStringHelper(this) + .add("origin", origin) + .add("target", target) + .toString(); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/TarjanComponentFinder.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/TarjanComponentFinder.java similarity index 97% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/TarjanComponentFinder.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/TarjanComponentFinder.java index 99933634a0..56336eefe9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/TarjanComponentFinder.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/TarjanComponentFinder.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; import java.util.ArrayList; import java.util.List; import com.google.common.primitives.Ints; -import static com.tngtech.archunit.library.dependencies.TarjanGraph.LESS_THAN_TWO_VALUES; +import static com.tngtech.archunit.library.cycle_detection.TarjanGraph.LESS_THAN_TWO_VALUES; import static java.util.Arrays.sort; import static java.util.Comparator.comparing; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/TarjanGraph.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/TarjanGraph.java similarity index 96% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/TarjanGraph.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/TarjanGraph.java index ac7784923c..911698c184 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/TarjanGraph.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/TarjanGraph.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; import java.util.Arrays; -import com.tngtech.archunit.library.dependencies.PrimitiveDataTypes.IntArray; -import com.tngtech.archunit.library.dependencies.PrimitiveDataTypes.IntStack; +import com.tngtech.archunit.library.cycle_detection.PrimitiveDataTypes.IntArray; +import com.tngtech.archunit.library.cycle_detection.PrimitiveDataTypes.IntStack; import static java.util.Arrays.fill; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/rules/CycleArchCondition.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/rules/CycleArchCondition.java new file mode 100644 index 0000000000..ed10129af2 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/rules/CycleArchCondition.java @@ -0,0 +1,378 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.cycle_detection.rules; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.SortedSetMultimap; +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvent; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import com.tngtech.archunit.library.cycle_detection.Cycle; +import com.tngtech.archunit.library.cycle_detection.CycleDetector; +import com.tngtech.archunit.library.cycle_detection.Cycles; +import com.tngtech.archunit.library.cycle_detection.Edge; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.MultimapBuilder.hashKeys; +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.library.cycle_detection.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; +import static com.tngtech.archunit.library.cycle_detection.rules.CycleRuleConfiguration.MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME; +import static java.lang.System.lineSeparator; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toCollection; + +/** + * A generic {@link ArchCondition} to check arbitrary {@code COMPONENT}s consisting of {@link JavaClass JavaClasses} + * for cyclic dependencies between those components (induced by the {@link Dependency dependencies} of the contained {@link JavaClass classes}).
+ * Construct it by following the fluent interface of {@link #builder()}. + * + * @param The type of the component to check dependencies between + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class CycleArchCondition extends ArchCondition { + private static final Logger log = LoggerFactory.getLogger(CycleArchCondition.class); + + private final Function> getClasses; + private final Function getDescription; + private final Function> getOutgoingDependencies; + private final Predicate relevantClassDependenciesPredicate; + private ClassesToComponentsMapping classesToComponentsMapping; + private ComponentCycleDetector cycleDetector; + private EventRecorder eventRecorder; + + @SuppressWarnings({"unchecked", "rawtypes"}) // Function is contra-variant in its input parameter + private CycleArchCondition( + Function> retrieveClasses, + Function retrieveDescription, + Function> retrieveOutgoingDependencies, + Predicate relevantClassDependenciesPredicate) { + super("be free of cycles"); + this.getClasses = (Function) retrieveClasses; + this.getDescription = (Function) retrieveDescription; + this.getOutgoingDependencies = (Function) retrieveOutgoingDependencies; + this.relevantClassDependenciesPredicate = (Predicate) relevantClassDependenciesPredicate; + } + + @Override + public void init(Collection allComponents) { + classesToComponentsMapping = new ClassesToComponentsMapping<>(allComponents, getClasses); + cycleDetector = new ComponentCycleDetector<>(allComponents); + eventRecorder = new EventRecorder<>(getDescription); + } + + @Override + public void check(COMPONENT component, ConditionEvents events) { + cycleDetector.addEdges(createComponentDependencies(component)); + } + + private Set> createComponentDependencies(COMPONENT component) { + SortedSetMultimap targetComponentsWithDependencies = targetsOf(component); + return sortedEntries(targetComponentsWithDependencies).stream() + .map(entry -> new ComponentDependency<>(component, entry.getKey(), entry.getValue())) + .collect(toImmutableSet()); + } + + private SortedSetMultimap targetsOf(COMPONENT component) { + SortedSetMultimap result = hashKeys().treeSetValues().build(); + getOutgoingDependencies.apply(component).stream() + .filter(relevantClassDependenciesPredicate) + .filter(dependency -> classesToComponentsMapping.containsKey(dependency.getTargetClass())) + .forEach(dependency -> result.put(classesToComponentsMapping.get(dependency.getTargetClass()), dependency)); + return result; + } + + // unfortunately SortedSetMultimap has no good API to iterate over all SortedSet values :-( + @SuppressWarnings({"unchecked", "rawtypes"}) + private Set>> sortedEntries(SortedSetMultimap multimap) { + return (Set) multimap.asMap().entrySet(); + } + + @Override + public void finish(ConditionEvents events) { + Cycles> cycles = cycleDetector.findCycles(); + if (cycles.maxNumberOfCyclesReached()) { + events.setInformationAboutNumberOfViolations(String.format( + " >= %d times - the maximum number of cycles to detect has been reached; " + + "this limit can be adapted using the `archunit.properties` value `%s=xxx`", + cycles.size(), MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME)); + } + for (Cycle> cycle : cycles) { + eventRecorder.record(cycle, events); + } + releaseResources(); + } + + private void releaseResources() { + classesToComponentsMapping = null; + cycleDetector = null; + eventRecorder = null; + } + + private static class ClassesToComponentsMapping { + private final Iterable allComponents; + private final Function> getClassesOfComponent; + private Map mapping; + + private ClassesToComponentsMapping(Iterable allComponents, Function> getClassesOfComponent) { + this.allComponents = allComponents; + this.getClassesOfComponent = getClassesOfComponent; + } + + public COMPONENT get(JavaClass javaClass) { + return mapping().get(javaClass); + } + + private Map mapping() { + if (mapping != null) { + return mapping; + } + ImmutableMap.Builder result = ImmutableMap.builder(); + for (COMPONENT component : allComponents) { + for (JavaClass javaClass : getClassesOfComponent.apply(component)) { + result.put(javaClass, component); + } + } + return mapping = result.build(); + } + + public boolean containsKey(JavaClass javaClass) { + return mapping().containsKey(javaClass); + } + } + + private static class ComponentCycleDetector { + private final Collection components; + private final Set> componentDependencies = new HashSet<>(); + + ComponentCycleDetector(Collection components) { + this.components = checkNotNull(components); + } + + void addEdges(Collection> componentDependencies) { + this.componentDependencies.addAll(componentDependencies); + } + + Cycles> findCycles() { + return CycleDetector.detectCycles(components, componentDependencies); + } + } + + private static class ComponentDependency implements Edge { + private final COMPONENT origin; + private final COMPONENT target; + private final SortedSet classDependencies; + + private ComponentDependency(COMPONENT origin, COMPONENT target, SortedSet classDependencies) { + this.origin = origin; + this.target = target; + this.classDependencies = classDependencies; + } + + @Override + public COMPONENT getOrigin() { + return origin; + } + + @Override + public COMPONENT getTarget() { + return target; + } + + SortedSet toClassDependencies() { + return classDependencies; + } + } + + private static class EventRecorder { + private static final String CYCLE_DETECTED_SECTION_INTRO = "Cycle detected: "; + private static final String CYCLE_EDGE_DESCRIPTION_SEPARATOR = " -> " + lineSeparator() + Strings.repeat(" ", CYCLE_DETECTED_SECTION_INTRO.length()); + private static final String DEPENDENCY_DETAILS_INDENT = Strings.repeat(" ", 4); + + private final CycleRuleConfiguration cycleConfiguration = new CycleRuleConfiguration(); + private final Function getDescriptionOfComponent; + + private EventRecorder(Function getDescriptionOfComponent) { + this.getDescriptionOfComponent = getDescriptionOfComponent; + log.trace("Maximum number of dependencies to report per edge is set to {}; " + + "this limit can be adapted using the `archunit.properties` value `{}=xxx`", + cycleConfiguration.getMaxNumberOfDependenciesToShowPerEdge(), MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME); + } + + void record(Cycle> cycle, ConditionEvents events) { + events.add(newEvent(cycle)); + } + + private ConditionEvent newEvent(Cycle> cycle) { + Map> descriptionsToEdges = sortEdgesByDescription(cycle); + String description = createDescription(descriptionsToEdges.keySet()); + String details = createDetails(descriptionsToEdges); + return SimpleConditionEvent.violated(cycle, CYCLE_DETECTED_SECTION_INTRO + description + lineSeparator() + details); + } + + private Map> sortEdgesByDescription(Cycle> cycle) { + LinkedList> edges = new LinkedList<>(cycle.getEdges()); + ComponentDependency startEdge = findStartEdge(cycle); + while (!edges.getFirst().equals(startEdge)) { + edges.addLast(edges.pollFirst()); + } + Map> descriptionToEdge = new LinkedHashMap<>(); + for (ComponentDependency edge : edges) { + descriptionToEdge.put(getDescriptionOfComponent.apply(edge.getOrigin()), edge); + } + return descriptionToEdge; + } + + // A cycle always has edges, so we know that there is always at least one edge and by that a minimum element + // with respect to comparing the description lexicographically + @SuppressWarnings("OptionalGetWithoutIsPresent") + private ComponentDependency findStartEdge(Cycle> cycle) { + return cycle.getEdges().stream().min(comparing(input -> getDescriptionOfComponent.apply(input.getOrigin()))).get(); + } + + private String createDescription(Collection edgeDescriptions) { + List descriptions = new ArrayList<>(edgeDescriptions); + descriptions.add(descriptions.get(0)); + return Joiner.on(CYCLE_EDGE_DESCRIPTION_SEPARATOR).join(descriptions); + } + + private String createDetails(Map> descriptionsToEdges) { + List details = new ArrayList<>(); + AtomicInteger componentIndex = new AtomicInteger(0); + descriptionsToEdges.forEach((description, dependencies) -> { + details.add(String.format(" %d. Dependencies of %s", componentIndex.incrementAndGet(), description)); + details.addAll(dependenciesDescription(dependencies)); + }); + return Joiner.on(lineSeparator()).join(details); + } + + private List dependenciesDescription(ComponentDependency edge) { + int maxDependencies = cycleConfiguration.getMaxNumberOfDependenciesToShowPerEdge(); + Collection allDependencies = edge.toClassDependencies(); + boolean tooManyDependenciesToDisplay = allDependencies.size() > maxDependencies; + + List result = allDependencies.stream() + .limit(maxDependencies) + .map(dependency -> DEPENDENCY_DETAILS_INDENT + "- " + dependency.getDescription()) + .collect(toCollection(ArrayList::new)); + if (tooManyDependenciesToDisplay) { + result.add(DEPENDENCY_DETAILS_INDENT + String.format("(%d further dependencies have been omitted...)", + allDependencies.size() - maxDependencies)); + } + return result; + } + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static NeedsRetrieveClasses builder() { + return new Builder<>(); + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class Builder implements NeedsRetrieveClasses, NeedsRetrieveDescription, NeedsRetrieveOutgoingDependencies { + private Function> retrieveClasses; + private Function retrieveDescription; + private Function> retrieveOutgoingDependencies; + private Predicate relevantClassDependenciesPredicate = __ -> true; + + private Builder() { + } + + @Override + public NeedsRetrieveDescription retrieveClassesBy(Function> retrieveClasses) { + this.retrieveClasses = checkNotNull(retrieveClasses); + return this; + } + + @Override + public NeedsRetrieveOutgoingDependencies retrieveDescriptionBy(Function retrieveDescription) { + this.retrieveDescription = checkNotNull(retrieveDescription); + return this; + } + + @Override + public Builder retrieveOutgoingDependenciesBy(Function> retrieveOutgoingDependencies) { + this.retrieveOutgoingDependencies = checkNotNull(retrieveOutgoingDependencies); + return this; + } + + /** + * @param relevantClassDependenciesPredicate A {@link Predicate} to decide which {@link Dependency dependencies} are relevant when checking for cycles + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Builder onlyConsiderDependencies(Predicate relevantClassDependenciesPredicate) { + this.relevantClassDependenciesPredicate = checkNotNull(relevantClassDependenciesPredicate); + return this; + } + + /** + * @return A new {@link CycleArchCondition} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public CycleArchCondition build() { + return new CycleArchCondition<>(retrieveClasses, retrieveDescription, retrieveOutgoingDependencies, relevantClassDependenciesPredicate); + } + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public interface NeedsRetrieveClasses { + /** + * @param retrieveClasses A {@link Function} to retrieve the contained {@link JavaClass classes} for any given {@code COMPONENT} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + NeedsRetrieveDescription retrieveClassesBy(Function> retrieveClasses); + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public interface NeedsRetrieveDescription { + /** + * @param retrieveDescription A {@link Function} to retrieve the description of a {@code COMPONENT} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + NeedsRetrieveOutgoingDependencies retrieveDescriptionBy(Function retrieveDescription); + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public interface NeedsRetrieveOutgoingDependencies { + /** + * @param retrieveOutgoingDependencies A {@link Function} to retrieve the outgoing {@link Dependency dependencies} of a {@code COMPONENT} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + Builder retrieveOutgoingDependenciesBy(Function> retrieveOutgoingDependencies); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/CycleConfiguration.java b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/rules/CycleRuleConfiguration.java similarity index 66% rename from archunit/src/main/java/com/tngtech/archunit/library/dependencies/CycleConfiguration.java rename to archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/rules/CycleRuleConfiguration.java index 43f60f11a7..f630defbe8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/CycleConfiguration.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/cycle_detection/rules/CycleRuleConfiguration.java @@ -13,34 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection.rules; import com.tngtech.archunit.ArchConfiguration; -final class CycleConfiguration { - static final String MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME = "cycles.maxNumberToDetect"; - private static final String MAX_NUMBER_OF_CYCLES_TO_DETECT_DEFAULT_VALUE = "100"; +final class CycleRuleConfiguration { static final String MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME = "cycles.maxNumberOfDependenciesPerEdge"; private static final String MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_DEFAULT_VALUE = "20"; - private final int maxCyclesToDetect; private final int maxDependenciesPerEdge; - CycleConfiguration() { - String configuredMaxCyclesToDetect = ArchConfiguration.get() - .getPropertyOrDefault(MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME, MAX_NUMBER_OF_CYCLES_TO_DETECT_DEFAULT_VALUE); - maxCyclesToDetect = Integer.parseInt(configuredMaxCyclesToDetect); - + CycleRuleConfiguration() { String configuredMaxDependenciesPerEdge = ArchConfiguration.get() .getPropertyOrDefault(MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME, MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_DEFAULT_VALUE); maxDependenciesPerEdge = Integer.parseInt(configuredMaxDependenciesPerEdge); } - int getMaxNumberOfCyclesToDetect() { - return maxCyclesToDetect; - } - int getMaxNumberOfDependenciesToShowPerEdge() { return maxDependenciesPerEdge; } diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Cycle.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Cycle.java deleted file mode 100644 index 086b6da5ef..0000000000 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Cycle.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2014-2023 TNG Technology Consulting GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.tngtech.archunit.library.dependencies; - -import java.util.List; -import java.util.Objects; - -import com.google.common.collect.ImmutableList; - -import static com.google.common.base.Preconditions.checkNotNull; - -class Cycle { - private final Path path; - - Cycle(List> edges) { - this(new Path<>(ImmutableList.copyOf(edges))); - } - - Cycle(Path path) { - this.path = checkNotNull(path); - validate(path); - } - - List> getEdges() { - return path.getEdges(); - } - - private void validate(Path path) { - if (path.isEmpty()) { - throwNoCycleException(path); - } - validateStartEqualsEnd(path); - } - - private void validateStartEqualsEnd(Path path) { - T edgeStart = path.getStart(); - T edgeEnd = path.getEnd(); - if (!edgeEnd.equals(edgeStart)) { - throwNoCycleException(path); - } - } - - private void throwNoCycleException(Path path) { - throw new IllegalArgumentException("The supplied edges do not form a cycle. Edges were " + path); - } - - @Override - public int hashCode() { - return Objects.hash(path.getSetOfEdges()); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Cycle other = (Cycle) obj; - return Objects.equals(this.path.getSetOfEdges(), other.path.getSetOfEdges()); - } - - @Override - public String toString() { - return "Cycle{" + path.edgesToString() + '}'; - } - - public static Cycle from(Path path) { - return new Cycle<>(path); - } -} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/GivenSlicesInternal.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/GivenSlicesInternal.java index 4b6a16d5c3..712e34b62e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/GivenSlicesInternal.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/GivenSlicesInternal.java @@ -15,13 +15,17 @@ */ package com.tngtech.archunit.library.dependencies; +import java.util.function.Function; + import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.base.HasDescription; import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.Priority; import com.tngtech.archunit.lang.SimpleConditionEvent; +import com.tngtech.archunit.library.cycle_detection.rules.CycleArchCondition; import com.tngtech.archunit.library.dependencies.syntax.GivenNamedSlices; import com.tngtech.archunit.library.dependencies.syntax.GivenSlices; import com.tngtech.archunit.library.dependencies.syntax.GivenSlicesConjunction; @@ -83,7 +87,13 @@ public SlicesShould should() { @Override public SliceRule beFreeOfCycles() { - return new SliceRule(classesTransformer, priority, (transformer, predicate) -> new SliceCycleArchCondition(predicate)); + return new SliceRule(classesTransformer, priority, (transformer, predicate) -> CycleArchCondition.builder() + .retrieveClassesBy(Function.identity()) + .retrieveDescriptionBy(HasDescription::getDescription) + .retrieveOutgoingDependenciesBy(Slice::getDependenciesFromSelf) + .onlyConsiderDependencies(predicate) + .build() + ); } @Override diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Graph.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Graph.java deleted file mode 100644 index 12a3222a0b..0000000000 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Graph.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2014-2023 TNG Technology Consulting GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.tngtech.archunit.library.dependencies; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ForwardingCollection; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimap; - -import static com.google.common.base.Preconditions.checkArgument; - -class Graph { - private final Map nodes = new HashMap<>(); - private final ListMultimap> outgoingEdges = ArrayListMultimap.create(); - - void addNodes(Iterable nodes) { - for (T node : nodes) { - if (!this.nodes.containsKey(node)) { - this.nodes.put(node, this.nodes.size()); - } - } - } - - void addEdges(Iterable> outgoingEdges) { - for (Edge edge : outgoingEdges) { - checkArgument(nodes.containsKey(edge.getFrom()), "Node %s of edge %s is not part of the graph", edge.getFrom(), edge); - checkArgument(nodes.containsKey(edge.getTo()), "Node %s of edge %s is not part of the graph", edge.getTo(), edge); - this.outgoingEdges.put(nodes.get(edge.getFrom()), edge); - } - } - - Cycles findCycles() { - Map>> edgesByTargetIndexByOriginIndex = indexEdgesByTargetIndexByOriginIndex(nodes, outgoingEdges); - JohnsonCycleFinder johnsonCycleFinder = new JohnsonCycleFinder(createPrimitiveGraph()); - ImmutableList.Builder> result = ImmutableList.builder(); - JohnsonCycleFinder.Result cycles = johnsonCycleFinder.findCycles(); - for (int[] rawCycle : cycles) { - result.add(mapToCycle(edgesByTargetIndexByOriginIndex, rawCycle)); - } - return new Cycles<>(result.build(), cycles.maxNumberOfCyclesReached()); - } - - private PrimitiveGraph createPrimitiveGraph() { - int[][] edges = new int[nodes.size()][]; - for (Map.Entry nodeToIndex : nodes.entrySet()) { - List> outgoing = outgoingEdges.get(nodeToIndex.getValue()); - edges[nodeToIndex.getValue()] = new int[outgoing.size()]; - for (int j = 0; j < outgoing.size(); j++) { - edges[nodeToIndex.getValue()][j] = nodes.get(outgoing.get(j).getTo()); - } - } - return new PrimitiveGraph(edges); - } - - private ImmutableMap>> indexEdgesByTargetIndexByOriginIndex( - Map nodes, - Multimap> outgoingEdges) { - - ImmutableMap.Builder>> edgeMapBuilder = ImmutableMap.builder(); - for (Map.Entry>> originIndexToEdges : outgoingEdges.asMap().entrySet()) { - ImmutableMap.Builder> targetIndexToEdges = ImmutableMap.builder(); - for (Edge edge : originIndexToEdges.getValue()) { - targetIndexToEdges.put(nodes.get(edge.getTo()), edge); - } - edgeMapBuilder.put(originIndexToEdges.getKey(), targetIndexToEdges.build()); - } - return edgeMapBuilder.build(); - } - - private Cycle mapToCycle(Map>> edgesByTargetIndexByOriginIndex, int[] rawCycle) { - ImmutableList.Builder> edges = ImmutableList.builder(); - int originIndex = -1; - for (int targetIndex : rawCycle) { - if (originIndex >= 0) { - edges.add(edgesByTargetIndexByOriginIndex.get(originIndex).get(targetIndex)); - } - originIndex = targetIndex; - } - edges.add(edgesByTargetIndexByOriginIndex.get(originIndex).get(rawCycle[0])); - return new Cycle<>(edges.build()); - } - - @Override - public String toString() { - return "Graph{" + - "nodes=" + nodes + - ", edges=" + outgoingEdges + - '}'; - } - - static class Cycles extends ForwardingCollection> { - private final Collection> cycles; - private final boolean maxNumberOfCyclesReached; - - private Cycles(Collection> cycles, boolean maxNumberOfCyclesReached) { - this.cycles = cycles; - this.maxNumberOfCyclesReached = maxNumberOfCyclesReached; - } - - boolean maxNumberOfCyclesReached() { - return maxNumberOfCyclesReached; - } - - @Override - protected Collection> delegate() { - return cycles; - } - } -} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceCycleArchCondition.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceCycleArchCondition.java deleted file mode 100644 index abf511b8dd..0000000000 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceCycleArchCondition.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2014-2023 TNG Technology Consulting GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.tngtech.archunit.library.dependencies; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; - -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import com.google.common.collect.ForwardingSet; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.SortedSetMultimap; -import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.core.domain.Dependency; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.lang.ArchCondition; -import com.tngtech.archunit.lang.ConditionEvent; -import com.tngtech.archunit.lang.ConditionEvents; -import com.tngtech.archunit.lang.SimpleConditionEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static com.google.common.collect.MultimapBuilder.hashKeys; -import static com.tngtech.archunit.library.dependencies.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; -import static com.tngtech.archunit.library.dependencies.CycleConfiguration.MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME; -import static java.lang.System.lineSeparator; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.toCollection; - -class SliceCycleArchCondition extends ArchCondition { - private static final Logger log = LoggerFactory.getLogger(SliceCycleArchCondition.class); - - private final DescribedPredicate predicate; - private ClassesToSlicesMapping classesToSlicesMapping; - private Graph graph; - private EventRecorder eventRecorder; - - SliceCycleArchCondition(DescribedPredicate predicate) { - super("be free of cycles"); - this.predicate = predicate; - } - - @Override - public void init(Collection allSlices) { - initializeResources(allSlices); - graph.addNodes(allSlices); - } - - private void initializeResources(Iterable allSlices) { - classesToSlicesMapping = new ClassesToSlicesMapping(allSlices); - graph = new Graph<>(); - eventRecorder = new EventRecorder(); - } - - @Override - public void check(Slice slice, ConditionEvents events) { - graph.addEdges(SliceDependencies.of(slice, classesToSlicesMapping, predicate)); - } - - @Override - public void finish(ConditionEvents events) { - Graph.Cycles cycles = graph.findCycles(); - if (cycles.maxNumberOfCyclesReached()) { - events.setInformationAboutNumberOfViolations(String.format( - " >= %d times - the maximum number of cycles to detect has been reached; " - + "this limit can be adapted using the `archunit.properties` value `%s=xxx`", - cycles.size(), MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME)); - } - for (Cycle cycle : cycles) { - eventRecorder.record(cycle, events); - } - releaseResources(); - } - - private void releaseResources() { - classesToSlicesMapping = null; - graph = null; - eventRecorder = null; - } - - private static class ClassesToSlicesMapping { - private final Iterable allSlices; - private Map mapping; - - private ClassesToSlicesMapping(Iterable allSlices) { - this.allSlices = allSlices; - } - - public Slice get(JavaClass javaClass) { - return mapping().get(javaClass); - } - - private Map mapping() { - if (mapping != null) { - return mapping; - } - ImmutableMap.Builder result = ImmutableMap.builder(); - for (Slice slice : allSlices) { - for (JavaClass javaClass : slice) { - result.put(javaClass, slice); - } - } - return mapping = result.build(); - } - - public boolean containsKey(JavaClass javaClass) { - return mapping().containsKey(javaClass); - } - } - - private static class SliceDependencies extends ForwardingSet> { - private final Set> edges; - - private SliceDependencies(Slice slice, ClassesToSlicesMapping classesToSlicesMapping, DescribedPredicate predicate) { - SortedSetMultimap targetSlicesWithDependencies = targetsOf(slice, classesToSlicesMapping, predicate); - ImmutableSet.Builder> edgeBuilder = ImmutableSet.builder(); - for (Map.Entry> entry : sortedEntries(targetSlicesWithDependencies)) { - edgeBuilder.add(new Edge<>(slice, entry.getKey(), entry.getValue())); - } - this.edges = edgeBuilder.build(); - } - - private SortedSetMultimap targetsOf(Slice slice, - ClassesToSlicesMapping classesToSlicesMapping, DescribedPredicate predicate) { - - SortedSetMultimap result = hashKeys().treeSetValues().build(); - slice.getDependenciesFromSelf().stream() - .filter(predicate) - .filter(dependency -> classesToSlicesMapping.containsKey(dependency.getTargetClass())) - .forEach(dependency -> result.put(classesToSlicesMapping.get(dependency.getTargetClass()), dependency)); - return result; - } - - // unfortunately SortedSetMultimap has no good API to iterate over all SortedSet values :-( - @SuppressWarnings({"unchecked", "rawtypes"}) - private Set>> sortedEntries(SortedSetMultimap multimap) { - return (Set) multimap.asMap().entrySet(); - } - - @Override - protected Set> delegate() { - return edges; - } - - static SliceDependencies of(Slice slice, ClassesToSlicesMapping classesToSlicesMapping, DescribedPredicate predicate) { - return new SliceDependencies(slice, classesToSlicesMapping, predicate); - } - } - - private static class EventRecorder { - private static final String CYCLE_DETECTED_SECTION_INTRO = "Cycle detected: "; - private static final String DEPENDENCY_DETAILS_INDENT = Strings.repeat(" ", 4); - - private final CycleConfiguration cycleConfiguration = new CycleConfiguration(); - - private EventRecorder() { - log.trace("Maximum number of dependencies to report per edge is set to {}; " - + "this limit can be adapted using the `archunit.properties` value `{}=xxx`", - cycleConfiguration.getMaxNumberOfDependenciesToShowPerEdge(), MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME); - } - - void record(Cycle cycle, ConditionEvents events) { - events.add(newEvent(cycle)); - } - - private ConditionEvent newEvent(Cycle cycle) { - Map> descriptionsToEdges = sortEdgesByDescription(cycle); - String description = createDescription(descriptionsToEdges.keySet(), CYCLE_DETECTED_SECTION_INTRO.length()); - String details = createDetails(descriptionsToEdges); - return new SimpleConditionEvent(cycle, - false, - CYCLE_DETECTED_SECTION_INTRO + description + lineSeparator() + details); - } - - private Map> sortEdgesByDescription(Cycle cycle) { - LinkedList> edges = new LinkedList<>(cycle.getEdges()); - Edge startEdge = cycle.getEdges().stream().min(comparing(input -> input.getFrom().getDescription())).get(); - while (!edges.getFirst().equals(startEdge)) { - edges.addLast(edges.pollFirst()); - } - Map> descriptionToEdge = new LinkedHashMap<>(); - for (Edge edge : edges) { - descriptionToEdge.put(edge.getFrom().getDescription(), edge); - } - return descriptionToEdge; - } - - private String createDescription(Collection edgeDescriptions, int indent) { - List descriptions = new ArrayList<>(edgeDescriptions); - descriptions.add(descriptions.get(0)); - return Joiner.on(" -> " + lineSeparator() + Strings.repeat(" ", indent)).join(descriptions); - } - - private String createDetails(Map> descriptionsToEdges) { - List details = new ArrayList<>(); - int sliceIndex = 0; - for (Map.Entry> edgeWithDescription : descriptionsToEdges.entrySet()) { - ++sliceIndex; - details.add(String.format(" %d. Dependencies of %s", sliceIndex, edgeWithDescription.getKey())); - details.addAll(dependenciesDescription(edgeWithDescription.getValue())); - } - return Joiner.on(lineSeparator()).join(details); - } - - private List dependenciesDescription(Edge edge) { - int maxDependencies = cycleConfiguration.getMaxNumberOfDependenciesToShowPerEdge(); - List allDependencies = edge.getAttachments(); - boolean tooManyDependenciesToDisplay = allDependencies.size() > maxDependencies; - List dependenciesToDisplay = tooManyDependenciesToDisplay ? allDependencies.subList(0, maxDependencies) : allDependencies; - - List result = dependenciesToDisplay.stream() - .map(dependency -> DEPENDENCY_DETAILS_INDENT + "- " + dependency.getDescription()) - .collect(toCollection(ArrayList::new)); - if (tooManyDependenciesToDisplay) { - result.add(DEPENDENCY_DETAILS_INDENT + String.format("(%d further dependencies have been omitted...)", - allDependencies.size() - dependenciesToDisplay.size())); - } - return result; - } - } -} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/AnnotationDescriptor.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/AnnotationDescriptor.java new file mode 100644 index 0000000000..24c4db1e63 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/AnnotationDescriptor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules; + +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.PublicAPI; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +/** + * An {@link ArchModule.Descriptor} that carries along a specific {@link Annotation}. + * @param The type of {@link Annotation} this {@link ArchModule.Descriptor} contains + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class AnnotationDescriptor implements ArchModule.Descriptor { + private final String name; + private final A annotation; + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public AnnotationDescriptor(String moduleName, A annotation) { + this.name = checkNotNull(moduleName); + this.annotation = checkNotNull(annotation); + } + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public String getName() { + return name; + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public A getAnnotation() { + return annotation; + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/ArchModule.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/ArchModule.java new file mode 100644 index 0000000000..2ba8951ff0 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/ArchModule.java @@ -0,0 +1,308 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.ForwardingSet; +import com.tngtech.archunit.base.Suppliers; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.properties.HasName; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; +import static java.util.Collections.emptyList; + +/** + * Represents a generic "architecture module", i.e. any group of classes that should form a cohesive unit.
+ * An {@link ArchModule} can be identified by its {@link #getIdentifier() identifier}. Vice versa an {@link ArchModule} + * can be defined as a mapping {@code JavaClass -> ArchModule.Identifier}, where all classes that are mapped to the + * same identifier will end up in the same module.
+ * {@link ArchModule} offers an API to obtain all {@link #getClassDependenciesFromSelf() class dependencies}, i.e. + * all {@link Dependency dependencies} from {@link JavaClass classes} within the module to {@link JavaClass classes} + * outside of the module. It also offers an API to obtain all {@link #getModuleDependenciesFromSelf() module dependencies}, + * i.e. dependencies from this {@link ArchModule} to another {@link ArchModule} where these dependencies reflect + * all {@link #getClassDependenciesFromSelf() class dependencies} where the origin resides within this {@link ArchModule} + * and the target resides within another {@link ArchModule}. + *

+ * To create {@link ArchModule}s please refer to {@link ArchModules}. + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class ArchModule extends ForwardingSet implements HasName { + private final Identifier identifier; + private final DESCRIPTOR descriptor; + private final Set classes; + private final Set classDependenciesFromSelf; + // resolving backwards dependencies is done lazily in JavaClass, so we don't trigger it eagerly here either + private final Supplier> classDependenciesToSelf; + private Set> moduleDependenciesFromSelf; + private Set> moduleDependenciesToSelf; + private Set undefinedDependencies; + + ArchModule(Identifier identifier, DESCRIPTOR descriptor, Set classes) { + this.identifier = checkNotNull(identifier); + this.descriptor = checkNotNull(descriptor); + this.classes = ImmutableSet.copyOf(classes); + classDependenciesFromSelf = classes.stream() + .flatMap(clazz -> clazz.getDirectDependenciesFromSelf().stream()) + .filter(dependency -> !classes.contains(dependency.getTargetClass().getBaseComponentType())) + .collect(toImmutableSet()); + classDependenciesToSelf = Suppliers.memoize(() -> classes.stream() + .flatMap(clazz -> clazz.getDirectDependenciesToSelf().stream()) + // we don't need the base component type here, because the origin of a dependency to this class will never be an array type + .filter(dependency -> !classes.contains(dependency.getOriginClass())) + .collect(toImmutableSet())); + } + + void setModuleDependencies(Set> moduleDependenciesFromSelf, Set> moduleDependenciesToSelf) { + this.moduleDependenciesFromSelf = ImmutableSet.copyOf(moduleDependenciesFromSelf); + this.moduleDependenciesToSelf = ImmutableSet.copyOf(moduleDependenciesToSelf); + this.undefinedDependencies = ImmutableSet.copyOf(Sets.difference(classDependenciesFromSelf, toClassDependencies(moduleDependenciesFromSelf))); + } + + private Set toClassDependencies(Set> moduleDependencies) { + return moduleDependencies.stream().flatMap(it -> it.toClassDependencies().stream()).collect(toImmutableSet()); + } + + @Override + protected Set delegate() { + return classes; + } + + /** + * @return The {@link Identifier} of this module + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Identifier getIdentifier() { + return identifier; + } + + /** + * @return The name of this module, i.e. a human-readable string representing this module + */ + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public String getName() { + return descriptor.getName(); + } + + /** + * @return The {@link ArchModule.Descriptor} of this {@link ArchModule} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public DESCRIPTOR getDescriptor() { + return descriptor; + } + + /** + * @return All {@link Dependency dependencies} where the {@link Dependency#getOriginClass() origin class} + * is contained within this {@link ArchModule}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Set getClassDependenciesFromSelf() { + return classDependenciesFromSelf; + } + + /** + * @return All {@link Dependency dependencies} where the {@link Dependency#getTargetClass() target class} + * is contained within this {@link ArchModule}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Set getClassDependenciesToSelf() { + return classDependenciesToSelf.get(); + } + + /** + * @return All {@link ModuleDependency module dependencies} where the {@link ModuleDependency#getOrigin() origin} + * equals this {@link ArchModule}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Set> getModuleDependenciesFromSelf() { + return moduleDependenciesFromSelf; + } + + /** + * @return All {@link ModuleDependency module dependencies} where the {@link ModuleDependency#getTarget() target} + * equals this {@link ArchModule}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Set> getModuleDependenciesToSelf() { + return moduleDependenciesToSelf; + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Set getUndefinedDependencies() { + return undefinedDependencies; + } + + @Override + public int hashCode() { + return Objects.hash(identifier); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (!super.equals(obj)) { + return false; + } + final ArchModule other = (ArchModule) obj; + return Objects.equals(this.identifier, other.identifier); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{identifier=" + getIdentifier() + ", name=" + getName() + '}'; + } + + /** + * An {@link Identifier} of an {@link ArchModule}. An {@link Identifier} is basically an ordered list of string parts that + * uniquely identifies an {@link ArchModule}, i.e. two {@link ArchModule modules} are equal, if and only if + * their identifier is equal (i.e. all the textual parts of the identifier match in order). + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class Identifier implements Iterable { + private final List parts; + + private Identifier(List parts) { + this.parts = ImmutableList.copyOf(parts); + } + + /** + * @see #from(List) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static Identifier from(String... parts) { + return from(ImmutableList.copyOf(parts)); + } + + /** + * @param parts The textual parts of the {@link Identifier}, must not be empty + * @return An {@link Identifier} consisting of the passed {@code parts} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static Identifier from(List parts) { + checkArgument(!parts.isEmpty(), "Parts may not be empty"); + return new Identifier(parts); + } + + /** + * Factory method to signal that this {@link ArchModule} is irrelevant. + * The {@link ArchModule} identified by this {@link Identifier} will e.g. be omitted when creating {@link ArchModules} + * and should i.g. be completely ignored for all purposes. + * + * @return An {@link Identifier} that signals that this {@link ArchModule} is irrelevant and should be ignored. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static Identifier ignore() { + return new Identifier(emptyList()); + } + + /** + * @return The number of (textual) parts this identifier consists of. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public int getNumberOfParts() { + return parts.size(); + } + + /** + * @param index Index of the requested (textual) part + * @return Part with the given index; indices are 1-based (i.e. {@link #getPart(int) getPart(1)}) returns the first part. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public String getPart(int index) { + checkArgument(index >= 1 && index <= parts.size(), "Index %d is out of bounds", index); + return parts.get(index - 1); + } + + boolean shouldBeConsidered() { + return !parts.isEmpty(); + } + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Iterator iterator() { + return parts.iterator(); + } + + @Override + public int hashCode() { + return Objects.hash(parts); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Identifier other = (Identifier) obj; + return Objects.equals(this.parts, other.parts); + } + + @Override + public String toString() { + return parts.toString(); + } + } + + /** + * Contains meta-information for an {@link ArchModule}. By default, this meta-information + * only contains the {@link ArchModule#getName() module name}, but it can be freely extended by users + * to transport more meta-information (e.g. allowed dependencies) when modularizing {@link JavaClasses} + * into {@link ArchModules}. + */ + @PublicAPI(usage = INHERITANCE) + public interface Descriptor { + /** + * @return The name of the respective {@link ArchModule} described by this {@link Descriptor} + */ + String getName(); + + /** + * Creates a default {@link Descriptor} only containing the passed {@code name} as {@link Descriptor#getName()}. + * + * @param name The name of the described {@link ArchModule} + * @return A {@link Descriptor} carrying the passed {@code name} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + static Descriptor create(final String name) { + return () -> name; + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/ArchModules.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/ArchModules.java new file mode 100644 index 0000000000..f22a9810fe --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/ArchModules.java @@ -0,0 +1,584 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.SetMultimap; +import com.google.common.collect.Sets; +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.ForwardingCollection; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.PackageMatcher; +import com.tngtech.archunit.library.dependencies.Slices; +import com.tngtech.archunit.library.modules.ArchModule.Identifier; +import com.tngtech.archunit.library.modules.ArchModules.Creator.WithGenericDescriptor; + +import static com.google.common.base.Functions.toStringFunction; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Multimaps.asMap; +import static com.google.common.collect.Multimaps.toMultimap; +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; +import static com.tngtech.archunit.core.domain.PackageMatcher.TO_GROUPS; +import static com.tngtech.archunit.library.modules.ArchModule.Identifier.ignore; +import static java.util.Collections.singleton; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toMap; + +/** + * A collection of {@link ArchModule "architectural modules"}. This class provides a convenient API to partition the {@link JavaClass classes} + * of a code base into (cohesive) modules and assert properties of these modules, e.g. their dependencies to each other or dependencies + * not contained in any of the constructed modules.
+ * This class provides several entry points to create {@link ArchModule modules} from a set of {@link JavaClass classes}:
+ *
    + *
  • {@link #defineBy(IdentifierAssociation)} - the most generic/flexible API
  • + *
  • {@link #defineByPackages(String)} - an API similar to {@link Slices#matching(String)}
  • + *
  • {@link #defineByRootClasses(Predicate)} - an API that derives modules from the packages of some specific classes
  • + *
  • {@link #defineByAnnotation(Class)} - a convenience API for {@link #defineByRootClasses(Predicate)} + * that picks the relevant classes by looking for an annotation
  • + *
+ */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class ArchModules extends ForwardingCollection> { + private final Map> modulesByIdentifier; + private final Map> modulesByName; + + private ArchModules(Set> modules) { + this.modulesByIdentifier = groupBy(modules, ArchModule::getIdentifier, "identifier"); + this.modulesByName = groupBy(modules, ArchModule::getName, "name"); + + SetMultimap> moduleDependenciesByOrigin = HashMultimap.create(); + modules.forEach(it -> moduleDependenciesByOrigin.putAll(it.getIdentifier(), createModuleDependencies(it, modules))); + + SetMultimap> moduleDependenciesByTarget = HashMultimap.create(); + moduleDependenciesByOrigin.values() + .forEach(moduleDependency -> moduleDependenciesByTarget.put(moduleDependency.getTarget().getIdentifier(), moduleDependency)); + + modules.forEach(it -> it.setModuleDependencies(moduleDependenciesByOrigin.get(it.getIdentifier()), moduleDependenciesByTarget.get(it.getIdentifier()))); + } + + private static Map> groupBy( + Set> modules, + Function, KEY> getKey, String keyName + ) { + Map>> modulesByKey = modules.stream().collect(toMultimap(getKey, identity(), HashMultimap::create)).asMap(); + SortedSet duplicateKeys = modulesByKey.entrySet().stream() + .filter(it -> it.getValue().size() > 1) + .map(Map.Entry::getKey) + .collect(toCollection(TreeSet::new)); + + if (!duplicateKeys.isEmpty()) { + throw new IllegalArgumentException(String.format("Found multiple modules with the same %s: %s", keyName, duplicateKeys)); + } + + return modulesByKey.entrySet().stream() + .collect(toMap(Map.Entry::getKey, entry -> getOnlyElement(entry.getValue()))); + } + + private ImmutableSet> createModuleDependencies(ArchModule origin, Set> modules) { + ImmutableSet.Builder> moduleDependencies = ImmutableSet.builder(); + for (ArchModule target : Sets.difference(modules, singleton(origin))) { + ModuleDependency.tryCreate(origin, target).ifPresent(moduleDependencies::add); + } + return moduleDependencies.build(); + } + + @Override + protected Collection> delegate() { + return modulesByIdentifier.values(); + } + + /** + * @param identifier The (textual) parts of an {@link ArchModule.Identifier}. + * @return The contained {@link ArchModule} having an {@link ArchModule.Identifier} comprised of the passed {@code identifier} parts. + * This method will throw an exception if no matching {@link ArchModule} is contained. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public ArchModule getByIdentifier(String... identifier) { + return tryGetByIdentifier(identifier).orElseThrow(() -> + new IllegalArgumentException(String.format("There is no %s with identifier %s", ArchModule.class.getSimpleName(), Arrays.toString(identifier)))); + } + + /** + * @param identifier The (textual) parts of an {@link ArchModule.Identifier}. + * @return The contained {@link ArchModule} having an {@link ArchModule.Identifier} comprised of the passed {@code identifier} parts, + * or {@link Optional#empty()} if no matching {@link ArchModule} is contained. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Optional> tryGetByIdentifier(String... identifier) { + return Optional.ofNullable(modulesByIdentifier.get(Identifier.from(identifier))); + } + + /** + * @param name The name of an {@link ArchModule} + * @return A contained {@link ArchModule} with the passed {@link ArchModule#getName() name}. + * This method will throw an exception if no matching {@link ArchModule} is contained. + * @see #tryGetByName(String) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public ArchModule getByName(String name) { + return tryGetByName(name).orElseThrow(() -> + new IllegalArgumentException(String.format("There is no %s with name %s", ArchModule.class.getSimpleName(), name))); + } + + /** + * @param name The name of an {@link ArchModule} + * @return A contained {@link ArchModule} with the passed {@link ArchModule#getName() name}, + * or {@link Optional#empty()} if no matching {@link ArchModule} is contained. + * @see #getByName(String) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Optional> tryGetByName(String name) { + return Optional.ofNullable(modulesByName.get(name)); + } + + /** + * @return The names of all {@link ArchModule modules} contained within these {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Set getNames() { + return modulesByName.keySet().stream().map(toStringFunction()).collect(toImmutableSet()); + } + + /** + * Entrypoint to create {@link ArchModules} by partitioning a set of {@link JavaClass classes} into specific packages + * matching the supplied {@code packageIdentifier} interpreted as {@link PackageMatcher}.
+ * + * Partitioning is done according to capturing groups. For example + *

+ * Suppose there are three classes:

+ * {@code com.example.module.one.SomeClass}
+ * {@code com.example.module.one.AnotherClass}
+ * {@code com.example.module.two.YetAnotherClass}

+ * If modules are created by specifying

+ * {@code ArchModules.defineByPackages("..module.(*)..").modularize(javaClasses)}

+ * then the result will be two {@link ArchModule modules}, the {@link ArchModule module} where the capturing group is 'one' + * and the {@link ArchModule module} where the capturing group is 'two'. The first {@link ArchModule module} will have + * an {@link ArchModule.Identifier} consisting of the single string {@code "one"}, while the latter will have + * an {@link ArchModule.Identifier} consisting of the single string {@code "two"}. + * If multiple packages would be matched, e.g. by {@code "..module.(*).(*).."}, the respective {@link ArchModule.Identifier} + * would contain the two matched (sub-)package names as its {@link ArchModule.Identifier#getPart(int) parts}. + *

+ * + * @param packageIdentifier A {@link PackageMatcher package identifier} + * @return A fluent API to further customize how to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static Creator defineByPackages(String packageIdentifier) { + return defineBy(identifierByPackage(packageIdentifier)); + } + + private static IdentifierAssociation identifierByPackage(String packageIdentifier) { + PackageMatcher packageMatcher = PackageMatcher.of(packageIdentifier); + return javaClass -> { + Optional result = packageMatcher.match(javaClass.getPackageName()); + return result.map(TO_GROUPS).map(Identifier::from).orElse(ignore()); + }; + } + + /** + * Entrypoint to create {@link ArchModules} by partitioning a set of {@link JavaClass classes} into packages + * defined by specific "root classes". The {@code rootClassPredicate} will determine which {@link JavaClass classes} + * are root classes. {@link ArchModules} are formed by grouping together all classes that reside in the same package + * or a subpackage of the respective root class. Thus, the packages of the defined root classes may not overlap, + * i.e. no root class must reside in the same or a subpackage of another root class. All {@link JavaClass classes} + * not contained in any package induced by a root class will be ignored from the derived {@link ArchModules}.
+ * + *

+ * Take for example the following three classes:

+ * {@code com.example.module.one.SomeClass}
+ * {@code com.example.module.one.AnotherClass}
+ * {@code com.example.module.two.SomeOtherClass}

+ * Then the {@code rootClassPredicate} + *


+     * javaClass -> javaClass.getSimpleName().startsWith("Some")
+     * 
+ * would pick the + * classes {@code SomeClass} and {@code SomeOtherClass} and derive the {@link ArchModules} from their packages, which + * in turn would put {@code SomeClass} and {@code AnotherClass} in the same {@link ArchModule} derived from {@code SomeClass}. + *

+ * + * @param rootClassPredicate A {@link Predicate} determining which {@link JavaClass} is a "root class", thus defining a + * {@link ArchModule} by its package + * @return A fluent API to further customize how to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static CreatorByRootClass defineByRootClasses(Predicate rootClassPredicate) { + return CreatorByRootClass.from(rootClassPredicate); + } + + /** + * Same as {@link #defineByAnnotation(Class, Function)}, but the name will be automatically derived from the {@code name} + * attribute of the respective annotation. I.e. to use this method the respective annotation must provide a name like in the following example: + *

+     *{@literal @}SomeExample(name = "Example Module")
+     * class SomeClass {}
+     * 
+ * In case the respective {@code annotationType} doesn't offer a name attribute like this please refer to {@link #defineByAnnotation(Class, Function)} instead. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static
WithGenericDescriptor> defineByAnnotation(Class annotationType) { + return defineByAnnotation(annotationType, input -> { + try { + return (String) input.annotationType().getMethod("name").invoke(input); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassCastException e) { + String message = String.format( + "Could not invoke @%s.name() -> Supplied annotation must provide a method 'String name()'. " + + "Otherwise use defineByAnnotation(annotationType, nameFunction).", input.annotationType().getSimpleName()); + throw new IllegalArgumentException(message, e); + } + }); + } + + /** + * Entrypoint to create {@link ArchModules} by partitioning a set of {@link JavaClass classes} into packages + * defined by "root classes" containing annotations of the given {@code annotationType}. + * This is basically a convenience function for {@link #defineByRootClasses(Predicate)} where the {@link Predicate} + * exactly identifies classes carrying the passed {@code annotationType} and the annotation will be carried + * forward into the derived {@link ArchModule}s by the derived {@link AnnotationDescriptor}.
+ *
+ * Take for example the following three classes:

+ * {@code @SomeAnnotation com.example.module.one.SomeClass}
+ * {@code com.example.module.one.AnotherClass}
+ * {@code @SomeAnnotation com.example.module.two.YetAnotherClass}

+ * Then + *

+     * ArchModules.defineByAnnotation(SomeAnnotation.class).modularize(javaClasses)
+     * 
+ * would pick the classes {@code SomeClass} and {@code YetAnotherClass}, since they are annotated with {@code SomeAnnotation}, + * and derive the {@link ArchModules} from their packages. This in turn would put {@code SomeClass} and {@code AnotherClass} + * in the same {@link ArchModule} derived from {@code SomeClass}. The final {@link ArchModule} would have a + * {@link ArchModule#getDescriptor() descriptor} of type {@link AnnotationDescriptor} from which the specific {@link Annotation} + * (i.e. instance of {@code @SomeAnnotation}) on {@code SomeClass} or {@code YetAnotherClass} could be obtained. + *
+ * As with {@link #defineByRootClasses(Predicate)} users of this method must make sure that packages of the classes + * annotated with the given {@code annotationType} don't overlap. + * + * @param annotationType The type of {@link Annotation} defining which {@link JavaClass} is a root class + * @param nameFunction A function determining how to derive the {@link ArchModule#getName() module name} from the respective annotation + * @return A fluent API to further customize how to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static
WithGenericDescriptor> defineByAnnotation(Class annotationType, Function nameFunction) { + return defineByRootClasses(it -> it.isAnnotatedWith(annotationType)) + .describeModuleByRootClass((__, rootClass) -> { + A annotation = rootClass.getAnnotationOfType(annotationType); + return new AnnotationDescriptor<>(nameFunction.apply(annotation), annotation); + } + ); + } + + /** + * Entrypoint to create {@link ArchModules} by a generic mapping function {@link JavaClass} -> {@link ArchModule.Identifier}. + * All {@link JavaClass classes} that are mapped to the same {@link ArchModule.Identifier} will end up in the same + * {@link ArchModule}.
+ * + * A simple example would be the {@code identifierFunction} + *

+     * javaClass -> ArchModule.Identifier.from(javaClass.getPackageName())
+     * 
+ * This would then create one {@link ArchModule} for each full package name and each {@link JavaClass} would + * be contained in the {@link ArchModule} where the {@link ArchModule.Identifier} coincides with the class's full package name. + * + * @param identifierFunction A function defining how each {@link JavaClass} is mapped to the respective {@link ArchModule.Identifier} + * @return A fluent API to further customize how to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static Creator defineBy(IdentifierAssociation identifierFunction) { + return new Creator(checkNotNull(identifierFunction)); + } + + /** + * Defines which {@link JavaClass classes} belong to the same {@link ArchModule.Identifier} and thus will eventually + * end up in the same {@link ArchModule}. + */ + @FunctionalInterface + @PublicAPI(usage = INHERITANCE) + public interface IdentifierAssociation { + /** + * An optional hook to add custom logic considering all {@link JavaClass classes} that will be associated with an + * {@link ArchModule.Identifier}, before {@link #associate(JavaClass)} will be called on any of these {@link JavaClass classes}. + */ + default void init(Collection allClasses) { + } + + /** + * Associates a {@link JavaClass} with a specific {@link ArchModule.Identifier} which will eventually put this + * {@link JavaClass} into the {@link ArchModule} with the respective {@link ArchModule.Identifier}. + * + * @param javaClass The {@link JavaClass} to associate with an {@link ArchModule.Identifier} + * @return The associated {@link ArchModule.Identifier} + */ + Identifier associate(JavaClass javaClass); + } + + /** + * A generic interface to be extended by users for providing custom implementations of {@link ArchModule.Descriptor} + * that can carry along more meta-information from the modularized {@link JavaClasses}. + * + * @param The type of the created {@link ArchModule.Descriptor} + */ + @FunctionalInterface + @PublicAPI(usage = INHERITANCE) + public interface DescriptorCreator { + + /** + * @param identifier The {@link ArchModule.Identifier} of the respective {@link ArchModule} + * @param containedClasses The {@link JavaClass classes} contained in the respective {@link ArchModule} + * @return A specific instance of a subtype of {@link ArchModule.Descriptor} + */ + DESCRIPTOR create(Identifier identifier, Set containedClasses); + } + + /** + * A more convenient {@link DescriptorCreator} tailored to the case that we + * {@link #defineByRootClasses(Predicate) define our modules by root classes}. Allows to derive the specific {@link ArchModule.Descriptor} + * directly from the root class that induced the respective {@link ArchModule}. + * + * @param A specific subtype of {@link ArchModule.Descriptor} + */ + @FunctionalInterface + @PublicAPI(usage = INHERITANCE) + public interface RootClassDescriptorCreator { + + /** + * @param identifier The {@link ArchModule.Identifier} of the respective {@link ArchModule} + * @param rootClass The {@link JavaClass root class} from which the respective {@link ArchModule} was derived + * @return A specific instance of a subtype of {@link ArchModule.Descriptor} + */ + DESCRIPTOR create(Identifier identifier, JavaClass rootClass); + } + + /** + * An element of the fluent API to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static class CreatorByRootClass extends Creator { + private final RootClassIdentifierAssociation identifierAssociation; + + private CreatorByRootClass(RootClassIdentifierAssociation identifierAssociation) { + super(identifierAssociation); + this.identifierAssociation = identifierAssociation; + } + + /** + * Allows to derive the {@link ArchModule.Descriptor} from the {@link JavaClass root class} that induced the respective {@link ArchModule}. + * + * @param descriptorCreator A function describing how to derive the {@link ArchModule.Descriptor} from the respective + * {@link ArchModule.Identifier} and {@link JavaClass root class} + * @param The specific subtype of {@link ArchModule.Descriptor} + * @return A fluent API to further customize how to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public WithGenericDescriptor describeModuleByRootClass(RootClassDescriptorCreator descriptorCreator) { + return describeBy((identifier, __) -> descriptorCreator.create(identifier, identifierAssociation.getRootClassOf(identifier))); + } + + static CreatorByRootClass from(Predicate rootClassPredicate) { + return new CreatorByRootClass(new RootClassIdentifierAssociation(rootClassPredicate)); + } + + private static class RootClassIdentifierAssociation implements IdentifierAssociation { + private final Map packageToIdentifier = new HashMap<>(); + private final Map identifierToRootClass = new HashMap<>(); + private final Predicate rootClassPredicate; + + private RootClassIdentifierAssociation(Predicate rootClassPredicate) { + this.rootClassPredicate = rootClassPredicate; + } + + @Override + public void init(Collection allClasses) { + allClasses.stream().filter(rootClassPredicate).forEach(rootClass -> { + packageToIdentifier.keySet().forEach(pkg -> { + if (packagesOverlap(pkg, rootClass.getPackageName())) { + throw new IllegalArgumentException(String.format( + "modules would overlap in '%s' and '%s'", pkg, rootClass.getPackageName())); + } + }); + Identifier identifier = Identifier.from(rootClass.getPackageName()); + packageToIdentifier.put(rootClass.getPackageName(), identifier); + identifierToRootClass.put(identifier, rootClass); + }); + } + + private boolean packagesOverlap(String firstPackageName, String secondPackageName) { + return packageContains(firstPackageName, secondPackageName) || packageContains(secondPackageName, firstPackageName); + } + + private boolean packageContains(String parentPackage, String childPackage) { + return childPackage.equals(parentPackage) || childPackage.startsWith(parentPackage + "."); + } + + @Override + public Identifier associate(JavaClass javaClass) { + return packageToIdentifier.entrySet().stream() + .filter(it -> packageContains(it.getKey(), javaClass.getPackageName())) + .findFirst() + .map(Map.Entry::getValue) + .orElse(Identifier.ignore()); + } + + JavaClass getRootClassOf(Identifier identifier) { + return identifierToRootClass.get(identifier); + } + } + } + + /** + * An element of the fluent API to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static class Creator { + private final IdentifierAssociation identifierAssociation; + private final Function deriveNameFunction; + + private Creator(IdentifierAssociation identifierAssociation) { + this(identifierAssociation, DEFAULT_NAMING_STRATEGY); + } + + private Creator(IdentifierAssociation identifierAssociation, Function deriveNameFunction) { + this.identifierAssociation = checkNotNull(identifierAssociation); + this.deriveNameFunction = checkNotNull(deriveNameFunction); + } + + /** + * Allows to customize each {@link ArchModule} {@link ArchModule#getName() name} by specifying a string pattern + * that defines how to derive the name from the {@link ArchModule.Identifier}.
+ * In particular, the passed {@code namingPattern} may contain numbered placeholders like ${1} + * to refer to parts from the {@link ArchModule.Identifier}. It may also contain the special placeholder + * $@ to refer to the colon-joined form of the identifier.
+ * Suppose the {@link ArchModule.Identifier} is {@code ["customer", "creation"]}, then the name could be derived as + *

+         * "Module[${1}/${2}]" -> would yield the name "Module[customer/creation]"
+         * "Module[$@]         -> would yield the name "Module[customer:creation]"
+         * 
+ * Note that the derived name must be unique between all {@link ArchModule modules}. + * + * @param namingPattern A string naming pattern deriving the {@link ArchModule} {@link ArchModule#getName() name} from + * the {@link ArchModule.Identifier} + * @return A fluent API to further customize how to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Creator deriveNameFromPattern(String namingPattern) { + return new Creator(identifierAssociation, identifier -> { + String result = namingPattern.replace("$@", joinIdentifier(identifier)); + for (int i = 1; i <= identifier.getNumberOfParts(); i++) { + result = result + .replace("$" + i, identifier.getPart(i)) + .replace("${" + i + "}", identifier.getPart(i)); + } + return result; + }); + } + + /** + * Allows to fully customize the {@link ArchModule.Descriptor} of the created {@link ArchModule}s. This allows to + * pass on meta-data from the contained classes additionally to any derived {@link ArchModule#getName() module name}. + * + * @param descriptorCreator A generic function specifying how to create the {@link ArchModule.Descriptor} + * @param The specific subtype of the {@link ArchModule.Descriptor} to create + * @return A fluent API to further customize how to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public WithGenericDescriptor describeBy(DescriptorCreator descriptorCreator) { + return new WithGenericDescriptor<>(identifierAssociation, descriptorCreator); + } + + /** + * @see WithGenericDescriptor#modularize(JavaClasses) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public ArchModules modularize(JavaClasses classes) { + return describeBy((identifier, __) -> ArchModule.Descriptor.create(deriveNameFunction.apply(identifier))) + .modularize(classes); + } + + private static final Function DEFAULT_NAMING_STRATEGY = Creator::joinIdentifier; + + private static String joinIdentifier(Identifier identifier) { + return Joiner.on(":").join(identifier); + } + + /** + * An element of the fluent API to create {@link ArchModules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class WithGenericDescriptor { + private final IdentifierAssociation identifierAssociation; + private final DescriptorCreator descriptorCreator; + + private WithGenericDescriptor(IdentifierAssociation identifierAssociation, DescriptorCreator descriptorCreator) { + this.identifierAssociation = checkNotNull(identifierAssociation); + this.descriptorCreator = checkNotNull(descriptorCreator); + } + + /** + * Derives {@link ArchModules} from the passed {@link JavaClasses} via the specified modularization strategy + * by the fluent API (e.g. by package identifier or by generic mapping function). In particular, + * the passed {@link JavaClasses} will be partitioned and sorted into matching instances of {@link ArchModule}. + * + * @param classes The classes to modularize + * @return An instance of {@link ArchModules} containing individual {@link ArchModule}s which in turn contain the partitioned classes + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public ArchModules modularize(JavaClasses classes) { + SetMultimap classesByIdentifier = groupClassesByIdentifier(classes); + + Set> modules = new HashSet<>(); + asMap(classesByIdentifier).forEach((identifier, containedClasses) -> { + DESCRIPTOR descriptor = descriptorCreator.create(identifier, containedClasses); + modules.add(new ArchModule<>(identifier, descriptor, containedClasses)); + }); + return new ArchModules<>(modules); + } + + private SetMultimap groupClassesByIdentifier(JavaClasses classes) { + identifierAssociation.init(classes); + + SetMultimap classesByIdentifier = HashMultimap.create(); + for (JavaClass javaClass : classes) { + Identifier identifier = identifierAssociation.associate(javaClass); + if (identifier.shouldBeConsidered()) { + classesByIdentifier.put(identifier, javaClass); + } + } + return classesByIdentifier; + } + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/ModuleDependency.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/ModuleDependency.java new file mode 100644 index 0000000000..2ebf18a533 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/ModuleDependency.java @@ -0,0 +1,127 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.domain.Dependency; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static java.lang.System.lineSeparator; +import static java.util.stream.Collectors.joining; + +/** + * A dependency between two {@link ArchModule}s. I.e. {@link #getOrigin() origin} and {@link #getTarget() target} + * are of type {@link ArchModule} and the dependency reflects the group of all {@link #toClassDependencies() class dependencies} + * where the {@link Dependency#getOriginClass() origin class} resides in the {@link #getOrigin() origin module} and the + * {@link Dependency#getTargetClass() target class} resides in the {@link #getTarget() target module}. + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class ModuleDependency implements HasDescription { + private final ArchModule origin; + private final ArchModule target; + private final Set classDependencies; + + private ModuleDependency(ArchModule origin, ArchModule target, Set classDependencies) { + this.origin = origin; + this.target = target; + this.classDependencies = classDependencies; + } + + /** + * @return The {@link ArchModule module} where this dependency originates from + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public ArchModule getOrigin() { + return origin; + } + + /** + * @return The {@link ArchModule module} that this dependency targets + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public ArchModule getTarget() { + return target; + } + + /** + * @return All the single {@link Dependency class dependencies} that form this {@link ModuleDependency}, + * i.e. all {@link Dependency dependencies} where the {@link Dependency#getOriginClass() origin class} + * resides in the {@link #getOrigin() origin module} and the {@link Dependency#getTargetClass() target class} + * resides in the {@link #getTarget() target module}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public Set toClassDependencies() { + return classDependencies; + } + + /** + * @return A textual representation of this {@link ModuleDependency} that can be used for textual reports. + */ + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public String getDescription() { + String classDependencyDescriptions = classDependencies.stream() + .map(HasDescription::getDescription) + .collect(joining(lineSeparator())); + return String.format("Module Dependency [%s -> %s]:%n%s", origin.getName(), target.getName(), classDependencyDescriptions); + } + + @Override + public int hashCode() { + return Objects.hash(origin.getIdentifier(), target.getIdentifier()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ModuleDependency other = (ModuleDependency) obj; + return Objects.equals(this.origin.getIdentifier(), other.origin.getIdentifier()) + && Objects.equals(this.target.getIdentifier(), other.target.getIdentifier()); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "origin=" + origin + + ", target=" + target + + '}'; + } + + static Optional> tryCreate(ArchModule origin, ArchModule target) { + Set classDependencies = filterDependenciesBetween(origin, target); + return !classDependencies.isEmpty() + ? Optional.of(new ModuleDependency<>(origin, target, classDependencies)) + : Optional.empty(); + } + + private static Set filterDependenciesBetween(ArchModule origin, ArchModule target) { + return origin.getClassDependenciesFromSelf().stream() + .filter(dependency -> target.contains(dependency.getTargetClass().getBaseComponentType())) + .collect(toImmutableSet()); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/AbstractGivenModulesInternal.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/AbstractGivenModulesInternal.java new file mode 100644 index 0000000000..b1d6689c9e --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/AbstractGivenModulesInternal.java @@ -0,0 +1,113 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.lang.annotation.Annotation; +import java.util.function.Function; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.lang.ArchCondition; +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.ArchModules; +import com.tngtech.archunit.library.modules.syntax.ModulesShouldInternal.ModulesByAnnotationShouldInternal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.all; + +abstract class AbstractGivenModulesInternal> implements GivenModules, GivenModulesConjunction { + private final ModulesTransformer transformer; + + private AbstractGivenModulesInternal(Function> createModules) { + this(new ModulesTransformer<>(createModules)); + } + + private AbstractGivenModulesInternal(ModulesTransformer transformer) { + this.transformer = checkNotNull(transformer); + } + + @Override + public ArchRule should(ArchCondition> condition) { + return all(transformer).should(condition); + } + + @Override + public ModulesShould should() { + return new ModulesShouldInternal<>(this::should); + } + + @Override + public SELF and(DescribedPredicate> predicate) { + return copy(transformer.and(predicate)); + } + + @Override + public SELF or(DescribedPredicate> predicate) { + return copy(transformer.or(predicate)); + } + + @Override + public SELF that(DescribedPredicate> predicate) { + return copy(transformer.that(predicate)); + } + + @Override + public SELF as(String description, Object... args) { + return copy(transformer.as(String.format(description, args))); + } + + abstract SELF copy(ModulesTransformer transformer); + + static class GivenModulesInternal extends AbstractGivenModulesInternal> { + GivenModulesInternal(Function> createModules) { + super(createModules); + } + + private GivenModulesInternal(ModulesTransformer transformer) { + super(transformer); + } + + @Override + GivenModulesInternal copy(ModulesTransformer transformer) { + return new GivenModulesInternal<>(transformer); + } + } + + static class GivenModulesByAnnotationInternal + extends AbstractGivenModulesInternal, GivenModulesByAnnotationInternal> + implements GivenModulesByAnnotation, GivenModulesByAnnotationConjunction { + + GivenModulesByAnnotationInternal(Function>> createModules) { + super(createModules); + } + + private GivenModulesByAnnotationInternal(ModulesTransformer> transformer) { + super(transformer); + } + + @Override + public ModulesByAnnotationShould should() { + return new ModulesByAnnotationShouldInternal<>(this::should); + } + + @Override + GivenModulesByAnnotationInternal copy(ModulesTransformer> transformer) { + return new GivenModulesByAnnotationInternal<>(transformer); + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/AllowedModuleDependencies.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/AllowedModuleDependencies.java new file mode 100644 index 0000000000..2be2e2d235 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/AllowedModuleDependencies.java @@ -0,0 +1,116 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.SetMultimap; +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ModuleDependency; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static java.util.stream.Collectors.joining; + +/** + * Defines which module may depend on which other modules by {@link ArchModule#getName() module name}.
+ * Start the definition by following the fluent API through {@link #allow()}.
+ * Extend the definition by calling {@link #fromModule(String)} multiple times. + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class AllowedModuleDependencies { + private final SetMultimap allowedModuleDependenciesByName = LinkedHashMultimap.create(); + + private AllowedModuleDependencies() { + } + + private AllowedModuleDependencies allowDependencies(String originModuleName, Set allowedTargetModuleNames) { + allowedModuleDependenciesByName.putAll(originModuleName, allowedTargetModuleNames); + return this; + } + + /** + * Adds allowed {@link ModuleDependency dependencies} that originate from the module with name {@code moduleName}. + * Finish this definition via {@link RequiringAllowedTargets#toModules(String...)}. + * + * @param moduleName A {@link ArchModule#getName() module name} + * @return An object that allows to specify the allowed targets for this module. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public RequiringAllowedTargets fromModule(String moduleName) { + return new RequiringAllowedTargets(moduleName); + } + + DescribedPredicate> asPredicate() { + return DescribedPredicate.describe( + getDescription(), + moduleDependency -> allowedModuleDependenciesByName.get(moduleDependency.getOrigin().getName()) + .contains(moduleDependency.getTarget().getName()) + ); + } + + private String getDescription() { + return allowedModuleDependenciesByName.asMap().entrySet().stream() + .map(originToTargets -> originToTargets.getKey() + " -> " + originToTargets.getValue()) + .collect(joining(", ", "{ ", " }")); + } + + /** + * Starts the definition of {@link AllowedModuleDependencies}. Follow up via {@link Creator#fromModule(String)}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static Creator allow() { + return new Creator(); + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class Creator { + private Creator() { + } + + /** + * @see AllowedModuleDependencies#fromModule(String) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public RequiringAllowedTargets fromModule(String moduleName) { + return new AllowedModuleDependencies().new RequiringAllowedTargets(moduleName); + } + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public final class RequiringAllowedTargets { + private final String originModuleName; + + private RequiringAllowedTargets(String originModuleName) { + this.originModuleName = originModuleName; + } + + /** + * Defines the allowed target {@link ArchModule#getName() module names} for the current origin {@link ArchModule#getName() module name} + * + * @param targetModuleNames An array of allowed target {@link ArchModule#getName() module names} + * @return {@link AllowedModuleDependencies} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public AllowedModuleDependencies toModules(String... targetModuleNames) { + return allowDependencies(originModuleName, ImmutableSet.copyOf(targetModuleNames)); + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/DescriptorFunction.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/DescriptorFunction.java new file mode 100644 index 0000000000..310009cb43 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/DescriptorFunction.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.Set; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ArchModules.DescriptorCreator; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; + +/** + * Serves the same purpose as {@link DescriptorCreator}, but carries along a {@link HasDescription#getDescription() description} + * to be used by rule syntax elements. + * + * @param The type of the {@link ArchModule.Descriptor} the respective {@link ArchModule}s will have + */ +@PublicAPI(usage = INHERITANCE) +public interface DescriptorFunction extends HasDescription { + + /** + * @see DescriptorCreator#create(ArchModule.Identifier, Set) + */ + DESCRIPTOR apply(ArchModule.Identifier identifier, Set containedClasses); + + /** + * Convenience method to create a {@link DescriptorFunction} from a {@link DescriptorCreator} and a textual {@code description}. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + static DescriptorFunction describe(String description, DescriptorCreator descriptorCreator) { + return new DescriptorFunction() { + @Override + public String getDescription() { + return description; + } + + @Override + public D apply(ArchModule.Identifier identifier, Set containedClasses) { + return descriptorCreator.create(identifier, containedClasses); + } + }; + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModules.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModules.java new file mode 100644 index 0000000000..1de43bb189 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModules.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.syntax.elements.GivenObjects; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface GivenModules extends GivenObjects> { + + @Override + GivenModulesConjunction that(DescribedPredicate> predicate); + + /** + * Allows to specify assertions for the set of {@link ArchModule}s under consideration. E.g. + *

+ * + * {@link ModuleRuleDefinition#modules() modules()}.{@link GivenModules#should() should()}.{@link ModulesShould#respectTheirAllowedDependencies(DescribedPredicate, ModuleDependencyScope) respectTheirAllowedDependencies(..)} + * + *
+ * Use {@link #should(ArchCondition)} to freely customize the condition against which the {@link ArchModule}s should be checked. + * + * @return A syntax element, which can be used to create rules for the {@link ArchModule}s under consideration + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesShould should(); + + /** + * Allows to adjust the description of the "given modules" part. E.g. + *

+     * modules().definedByAnnotation(AppModule.class).as("App Modules").should()...
+     * 
+ * would yield a rule text "App Modules should...". + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + GivenModules as(String description, Object... args); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesByAnnotation.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesByAnnotation.java new file mode 100644 index 0000000000..94bee8c4cf --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesByAnnotation.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface GivenModulesByAnnotation extends GivenModules> { + + @Override + GivenModulesByAnnotationConjunction that(DescribedPredicate>> predicate); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationShould should(); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesByAnnotationConjunction.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesByAnnotationConjunction.java new file mode 100644 index 0000000000..471d3f4c21 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesByAnnotationConjunction.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface GivenModulesByAnnotationConjunction extends GivenModulesConjunction> { + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + GivenModulesByAnnotationConjunction and(DescribedPredicate>> predicate); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + GivenModulesByAnnotationConjunction or(DescribedPredicate>> predicate); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationShould should(); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + GivenModulesByAnnotationConjunction as(String description, Object... args); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesConjunction.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesConjunction.java new file mode 100644 index 0000000000..f5eb12b3bc --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/GivenModulesConjunction.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.lang.syntax.elements.GivenConjunction; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface GivenModulesConjunction extends GivenConjunction> { + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + GivenModulesConjunction and(DescribedPredicate> predicate); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + GivenModulesConjunction or(DescribedPredicate> predicate); + + /** + * @see GivenModules#should() + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesShould should(); + + /** + * @see GivenModules#as(String, Object...) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + GivenModulesConjunction as(String description, Object... args); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModuleDependencyScope.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModuleDependencyScope.java new file mode 100644 index 0000000000..0d87c609c7 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModuleDependencyScope.java @@ -0,0 +1,113 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.PackageMatcher; +import com.tngtech.archunit.core.domain.PackageMatchers; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +/** + * Used to specify which dependencies should be checked by the respective {@link ArchRule}. Possible options are: + *
    + *
  • {@link #consideringAllDependencies()}
  • + *
  • {@link #consideringOnlyDependenciesBetweenModules()}
  • + *
  • {@link #consideringOnlyDependenciesInAnyPackage(String, String...)}
  • + *
+ */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class ModuleDependencyScope implements HasDescription { + private final String description; + private final Function>, Predicate> createPredicate; + + private ModuleDependencyScope(String description, Function>, Predicate> createPredicate) { + this.description = checkNotNull(description); + this.createPredicate = checkNotNull(createPredicate); + } + + @Override + public String getDescription() { + return description; + } + + @SuppressWarnings("unchecked") + Predicate asPredicate(Collection> modules) { + return createPredicate.apply((Collection>) modules); + } + + /** + * Considers all dependencies of every imported class, including basic Java classes like {@link Object}. + * + * @see #consideringOnlyDependenciesBetweenModules() + * @see #consideringOnlyDependenciesInAnyPackage(String, String...) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static ModuleDependencyScope consideringAllDependencies() { + return new ModuleDependencyScope("considering all dependencies", __ -> ___ -> true); + } + + /** + * Considers only dependencies of the imported classes between two modules. + * I.e. origin and target classes of the dependency must be contained within modules under test to be considered. + * This makes it easy to ignore dependencies to irrelevant classes like {@link Object}, but bears the + * danger of missing dependencies to classes that are falsely not covered by the declared module structure. + * + * @see #consideringAllDependencies() + * @see #consideringOnlyDependenciesInAnyPackage(String, String...) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static ModuleDependencyScope consideringOnlyDependenciesBetweenModules() { + return new ModuleDependencyScope( + "considering only dependencies between modules", + modules -> dependency -> modules.stream().anyMatch(it -> it.contains(dependency.getTargetClass())) + ); + } + + /** + * Considers only dependencies of imported classes that target packages matching the given the {@link PackageMatcher package identifiers}. + * This can for example be used to limit checked dependencies to those contained in the own project, + * e.g. 'com.myapp..'.
+ * Note that module dependencies, i.e. dependencies between two modules, will never be filtered, + * so this {@link ModuleDependencyScope} will never limit the amount of considered dependencies + * more than {@link #consideringOnlyDependenciesBetweenModules()}. + * + * @see #consideringAllDependencies() + * @see #consideringOnlyDependenciesBetweenModules() + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static ModuleDependencyScope consideringOnlyDependenciesInAnyPackage(String packageIdentifier, String... furtherPackageIdentifiers) { + List packageIdentifiers = Stream.concat(Stream.of(packageIdentifier), stream(furtherPackageIdentifiers)).collect(toList()); + PackageMatchers packageMatchers = PackageMatchers.of(packageIdentifiers); + String description = String.format("considering only dependencies in any package [%s]", joinSingleQuoted(packageIdentifiers)); + return new ModuleDependencyScope(description, __ -> dependency -> packageMatchers.test(dependency.getTargetClass().getPackageName())); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModuleRuleDefinition.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModuleRuleDefinition.java new file mode 100644 index 0000000000..41950163d5 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModuleRuleDefinition.java @@ -0,0 +1,230 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.lang.annotation.Annotation; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ArchModules; +import com.tngtech.archunit.library.modules.syntax.AbstractGivenModulesInternal.GivenModulesByAnnotationInternal; +import com.tngtech.archunit.library.modules.syntax.AbstractGivenModulesInternal.GivenModulesInternal; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +/** + * @see #modules() + */ +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public final class ModuleRuleDefinition { + private ModuleRuleDefinition() { + } + + /** + * Entrypoint to define {@link ArchRule rules} based on {@link ArchModules}. + * + * @return A syntax element to create {@link ArchModules} {@link ArchRule rules} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static Creator modules() { + return new Creator(); + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class Creator { + private Creator() { + } + + /** + * @see ArchModules#defineBy(ArchModules.IdentifierAssociation) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public GenericDefinition definedBy(DescribedFunction identifierFunction) { + return new GenericDefinition(identifierFunction); + } + + /** + * @see ArchModules#defineByRootClasses(Predicate) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public RootClassesDefinition definedByRootClasses(DescribedPredicate predicate) { + return RootClassesDefinition.create(predicate); + } + + /** + * @see ArchModules#defineByAnnotation(Class) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public
GivenModulesByAnnotation definedByAnnotation(Class annotationType) { + return new GivenModulesByAnnotationInternal<>(classes -> ArchModules + .defineByAnnotation(annotationType) + .modularize(classes) + ).as("modules defined by annotation @%s", annotationType.getSimpleName()); + } + + /** + * @see ArchModules#defineByPackages(String) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public PackagesDefinition definedByPackages(String packageIdentifier) { + return new PackagesDefinition(packageIdentifier); + } + + /** + * @see ArchModules#defineByAnnotation(Class, Function) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public GivenModulesByAnnotation definedByAnnotation(Class annotationType, Function nameFunction) { + return new GivenModulesByAnnotationInternal<>(classes -> ArchModules + .defineByAnnotation(annotationType, nameFunction) + .modularize(classes) + ).as("modules defined by annotation @%s", annotationType.getSimpleName()); + } + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class RootClassesDefinition implements GivenModules { + private final Predicate rootClassPredicate; + private final Function descriptorFunction; + private final String description; + + private RootClassesDefinition(Predicate rootClassPredicate, Function descriptorFunction, String description) { + this.rootClassPredicate = rootClassPredicate; + this.descriptorFunction = descriptorFunction; + this.description = description; + } + + /** + * @see ArchModules.CreatorByRootClass#describeModuleByRootClass(ArchModules.RootClassDescriptorCreator) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public GivenModules derivingModuleFromRootClassBy(DescribedFunction descriptorFunction) { + return new RootClassesDefinition<>(rootClassPredicate, descriptorFunction, description + " deriving module from root class by " + descriptorFunction.getDescription()); + } + + @Override + public ArchRule should(ArchCondition> condition) { + return newGivenModules().should(condition); + } + + @Override + public GivenModulesConjunction that(DescribedPredicate> predicate) { + return newGivenModules().that(predicate); + } + + @Override + public ModulesShould should() { + return newGivenModules().should(); + } + + @Override + public GivenModules as(String description, Object... args) { + return newGivenModules().as(description, args); + } + + private GivenModules newGivenModules() { + return new GivenModulesInternal<>(classes -> ArchModules + .defineByRootClasses(rootClassPredicate) + .describeModuleByRootClass((__, rootClass) -> descriptorFunction.apply(rootClass)) + .modularize(classes) + ).as(description); + } + + private static RootClassesDefinition create(DescribedPredicate rootClassPredicate) { + return new RootClassesDefinition<>( + rootClassPredicate, + javaClass -> ArchModule.Descriptor.create(javaClass.getPackageName()), + "modules defined by root classes " + rootClassPredicate.getDescription()); + } + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class PackagesDefinition implements GivenModules { + private final String description; + private final ArchModules.Creator creator; + + PackagesDefinition(String packageIdentifier) { + this(ArchModules.defineByPackages(packageIdentifier), String.format("modules defined by packages '%s'", packageIdentifier)); + } + + private PackagesDefinition(ArchModules.Creator creator, String description) { + this.creator = creator; + this.description = description; + } + + @SuppressWarnings("unchecked") // The descriptor of ArchModules is covariant, so we can always "upcast" + private GivenModules newGivenModules() { + return new GivenModulesInternal<>(classes -> (ArchModules) creator.modularize(classes)).as(description); + } + + /** + * @see ArchModules.Creator#deriveNameFromPattern(String) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public PackagesDefinition derivingNameFromPattern(String namePattern) { + return new PackagesDefinition(creator.deriveNameFromPattern(namePattern), description + String.format(" deriving name from pattern '%s'", namePattern)); + } + + @Override + public ArchRule should(ArchCondition> condition) { + return newGivenModules().should(condition); + } + + @Override + public GivenModulesConjunction that(DescribedPredicate> predicate) { + return newGivenModules().that(predicate); + } + + @Override + public ModulesShould should() { + return newGivenModules().should(); + } + + @Override + public GivenModules as(String description, Object... args) { + return newGivenModules().as(description, args); + } + } + + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public static final class GenericDefinition { + private final DescribedFunction identifierFunction; + + private GenericDefinition(DescribedFunction identifierFunction) { + this.identifierFunction = identifierFunction; + } + + /** + * @see ArchModules.Creator#describeBy(ArchModules.DescriptorCreator) + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + public GivenModules derivingModule(DescriptorFunction descriptorFunction) { + return new GivenModulesInternal<>(classes -> ArchModules + .defineBy(identifierFunction::apply) + .describeBy(descriptorFunction::apply) + .modularize(classes) + ).as("modules defined by %s deriving module %s", identifierFunction.getDescription(), descriptorFunction.getDescription()); + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesByAnnotationRule.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesByAnnotationRule.java new file mode 100644 index 0000000000..3a79d8b420 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesByAnnotationRule.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.lang.annotation.Annotation; +import java.util.function.Predicate; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface ModulesByAnnotationRule extends ModulesRule> { + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationShould andShould(); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule as(String newDescription); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule because(String reason); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule allowEmptyShould(boolean allowEmptyShould); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule ignoreDependency(Class origin, Class target); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule ignoreDependency(String originFullyQualifiedClassName, String targetFullyQualifiedClassName); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule ignoreDependency(Predicate originPredicate, Predicate targetPredicate); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule ignoreDependency(Predicate dependencyPredicate); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesByAnnotationShould.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesByAnnotationShould.java new file mode 100644 index 0000000000..591287768a --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesByAnnotationShould.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.PackageMatcher; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface ModulesByAnnotationShould extends ModulesShould> { + + /** + * Like {@link #respectTheirAllowedDependencies(DescribedPredicate, ModuleDependencyScope)}, but the allowed dependencies will be automatically + * derived from the {@link ANNOTATION} property named {@code annotationPropertyName}. This property *must* be of type {@code String[]} + * and contain the {@link ArchModule#getName() names} of the {@link ArchModule}s to which access is allowed. + *

+ * For example, given the user-defined annotation + *

+     * @interface MyModule {
+     *   String name();
+     *
+     *   String[] allowedDependencies() default {};
+     * }
+     * 
+ * and the annotated root classes + *

+     * @MyModule(name = "Module One", allowedDependencies = {"Module Two", "Module Three"})
+     * interface ModuleOneDescriptor {}
+     *
+     * @MyModule(name = "Module Two", allowedDependencies = {"Module Three"})
+     * interface ModuleTwoDescriptor {}
+     *
+     * @MyModule(name = "Module Three")
+     * interface ModuleThreeDescriptor {}
+     * 
+ * Then the allowed dependencies between the modules would be + *

+     * ,----------.
+     * |Module One| ------------.
+     * `----------'              \
+     *      |                    |
+     *      |                    |
+     *      v                    v
+     * ,----------.         ,------------.
+     * |Module Two| ------> |Module Three|
+     * `----------'         `------------'
+     * 
+ * + * @param annotationPropertyName The name of the property declared within {@link ANNOTATION} that declares allowed dependencies to other {@link ArchModule}s by name + * @param dependencyScope Allows to adjust which {@link Dependency dependencies} are considered relevant by the rule + * @return An {@link ArchRule} to be checked against a set of {@link JavaClasses} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule respectTheirAllowedDependenciesDeclaredIn(String annotationPropertyName, ModuleDependencyScope dependencyScope); + + /** + * Checks that the {@link Dependency#getTargetClass() target classes} of each {@link Dependency} + * that originate from one {@link ArchModule} and target another {@link ArchModule} reside in a package that matches + * a package identifier declared within {@link ANNOTATION}. + *

+ * For example, given the annotation + *

+     * @interface MyModule {
+     *   String name();
+     *
+     *   String[] exposedPackages() default {};
+     * }
+     * 
+ * and the annotated root classes + *

+     * @MyModule(name = "Module One")
+     * interface ModuleOneDescriptor {}
+     *
+     * @MyModule(name = "Module Two", exposedPackages = {"com.myapp.module_two.api.."})
+     * interface ModuleTwoDescriptor {}
+     * 
+ * Then a dependency from Module One to a class {@code com.myapp.module_two.api.SomeApi} + * would be allowed, but a dependency to a class + * {@code com.myapp.module_two.OutsideOfApi} would be forbidden. + * + * @param annotationPropertyName The name of the property declared within {@link ANNOTATION} that defines through which + * {@link PackageMatcher package identifiers} modules may depend on each other + * @return An {@link ArchRule} to be checked against a set of {@link JavaClasses} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesByAnnotationRule onlyDependOnEachOtherThroughPackagesDeclaredIn(String annotationPropertyName); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesRule.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesRule.java new file mode 100644 index 0000000000..f7f51c20cb --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesRule.java @@ -0,0 +1,119 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.function.Predicate; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface ModulesRule extends ArchRule, ModulesShouldConjunction { + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule as(String newDescription); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule because(String reason); + + @Override + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule allowEmptyShould(boolean allowEmptyShould); + + /** + * Ignores all class dependencies from the given origin class to the given target class. + *

+ * Note that this will always refer to the last {@link ArchCondition} only if multiple + * {@link ArchCondition}s are joined using {@link #andShould()}. E.g. + *

+     * modules()...
+     *   .should().firstCondition()
+     *   .ignoreDependency(/* will only refer to `firstCondition`, not to `secondCondition` */)
+     *   .andShould().secondCondition()
+     *   .ignoreDependency(/* will only refer to `secondCondition`, not to `firstCondition` */)
+     * 
+ * + * @param origin the origin class of dependencies to ignore + * @param target the target class of dependencies to ignore + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule ignoreDependency(Class origin, Class target); + + /** + * Ignores all class dependencies from the origin class with the given fully qualified origin class name + * to the target class with the given fully qualified target class name. + *

+ * Note that this will always refer to the last {@link ArchCondition} only if multiple + * {@link ArchCondition}s are joined using {@link #andShould()}. E.g. + *

+     * modules()...
+     *   .should().firstCondition()
+     *   .ignoreDependency(/* will only refer to `firstCondition`, not to `secondCondition` */)
+     *   .andShould().secondCondition()
+     *   .ignoreDependency(/* will only refer to `secondCondition`, not to `firstCondition` */)
+     * 
+ * + * @param originFullyQualifiedClassName the fully qualified origin class of dependencies to ignore + * @param targetFullyQualifiedClassName the fully qualified target class of dependencies to ignore + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule ignoreDependency(String originFullyQualifiedClassName, String targetFullyQualifiedClassName); + + /** + * Ignores all class dependencies from any origin class matching the given origin class predicate + * to any target class matching the given target class predicate. + *

+ * Note that this will always refer to the last {@link ArchCondition} only if multiple + * {@link ArchCondition}s are joined using {@link #andShould()}. E.g. + *

+     * modules()...
+     *   .should().firstCondition()
+     *   .ignoreDependency(/* will only refer to `firstCondition`, not to `secondCondition` */)
+     *   .andShould().secondCondition()
+     *   .ignoreDependency(/* will only refer to `secondCondition`, not to `firstCondition` */)
+     * 
+ * + * @param originPredicate predicate determining for which origins of dependencies the dependency should be ignored + * @param targetPredicate predicate determining for which targets of dependencies the dependency should be ignored + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule ignoreDependency(Predicate originPredicate, Predicate targetPredicate); + + /** + * Ignores all class dependencies matching the given predicate. + *

+ * Note that this will always refer to the last {@link ArchCondition} only if multiple + * {@link ArchCondition}s are joined using {@link #andShould()}. E.g. + *

+     * modules()...
+     *   .should().firstCondition()
+     *   .ignoreDependency(/* will only refer to `firstCondition`, not to `secondCondition` */)
+     *   .andShould().secondCondition()
+     *   .ignoreDependency(/* will only refer to `secondCondition`, not to `firstCondition` */)
+     * 
+ */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule ignoreDependency(Predicate dependencyPredicate); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShould.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShould.java new file mode 100644 index 0000000000..d205d153e3 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShould.java @@ -0,0 +1,97 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.syntax.elements.ClassesThat; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ModuleDependency; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface ModulesShould { + + /** + * Creates a rule to check that the {@link ArchModule}s under consideration don't have any forbidden + * {@link ArchModule#getModuleDependenciesFromSelf() module dependencies} according to the passed + * {@code allowedDependencyPredicate}. It is possible to adjust which {@link Dependency class dependencies} + * the rule considers relevant by the passed {@link ModuleDependencyScope dependencyScope}. + * + * @param allowedDependencyPredicate Decides which {@link ModuleDependency module dependencies} are allowed. + * If the {@link DescribedPredicate} returns {@code true} the dependency is allowed, + * otherwise it is forbidden. + * @param dependencyScope Allows to adjust which {@link Dependency dependencies} are considered relevant by the rule + * @return An {@link ArchRule} to be checked against a set of {@link JavaClasses} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule respectTheirAllowedDependencies( + DescribedPredicate> allowedDependencyPredicate, + ModuleDependencyScope dependencyScope + ); + + /** + * Convenience API for {@link #respectTheirAllowedDependencies(DescribedPredicate, ModuleDependencyScope)} + * that allows to statically define which {@link ArchModule modules} may depend on which other {@link ArchModule modules} + * by {@link ArchModule#getName() module name}. + *

+ * Example: + *

+     * modules()
+     *   .definedByPackages("..example.(*)..")
+     *   .should().respectTheirAllowedDependencies(
+     *     AllowedModuleDependencies.allow()
+     *       .fromModule("Module One").toModules("Module Three", "Module Four")
+     *       .fromModule("Module Two").toModules("Module Four"),
+     *     consideringOnlyDependenciesInAnyPackage("..example.."));
+     * 
+ */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule respectTheirAllowedDependencies(AllowedModuleDependencies allowedDependencies, ModuleDependencyScope dependencyScope); + + /** + * Checks that the {@link Dependency#getTargetClass() target classes} of each {@link Dependency} + * that originate from one {@link ArchModule} and target another {@link ArchModule} satisfy + * the passed {@code predicate}. + * + * @param predicate A {@link DescribedPredicate} to determine which {@link Dependency#getTargetClass() target classes} + * of {@link Dependency dependencies} are allowed. + * @return An {@link ArchRule} to be checked against a set of {@link JavaClasses} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule onlyDependOnEachOtherThroughClassesThat(DescribedPredicate predicate); + + /** + * Like {@link #onlyDependOnEachOtherThroughClassesThat(DescribedPredicate)} but allows to specify the predicate in a fluent way. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ClassesThat> onlyDependOnEachOtherThroughClassesThat(); + + /** + * Checks that the {@link ArchModule}s under consideration don't have any cyclic dependencies within their + * {@link ArchModule#getModuleDependenciesFromSelf() module dependencies}. + * + * @return An {@link ArchRule} to be checked against a set of {@link JavaClasses} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesRule beFreeOfCycles(); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldConjunction.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldConjunction.java new file mode 100644 index 0000000000..3d66613b9e --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldConjunction.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.ArchModule; + +import static com.tngtech.archunit.PublicAPI.State.EXPERIMENTAL; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS, state = EXPERIMENTAL) +public interface ModulesShouldConjunction { + + /** + * Like {@link #andShould(ArchCondition)} but offers a fluent API to pick the condition to join. + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ModulesShould andShould(); + + /** + * Joins another condition to this rule with {@code and} semantics. That is, all modules under test + * now needs to satisfy the existing condition and this new one.
+ * Note that this is always left-associative and does not support any operator precedence. + * + * @param condition Another condition to be 'and'-ed to the current condition of this rule + * @return An {@link ArchRule} to check against imported {@link JavaClasses} + */ + @PublicAPI(usage = ACCESS, state = EXPERIMENTAL) + ArchRule andShould(ArchCondition> condition); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldInternal.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldInternal.java new file mode 100644 index 0000000000..82b8ddbd8d --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldInternal.java @@ -0,0 +1,394 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.EvaluationResult; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import com.tngtech.archunit.lang.syntax.ClassesThatInternal; +import com.tngtech.archunit.lang.syntax.elements.ClassesThat; +import com.tngtech.archunit.library.cycle_detection.rules.CycleArchCondition; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ModuleDependency; + +import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage; +import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; + +class ModulesShouldInternal implements ModulesShould { + final Function>, ArchRule> createRule; + + ModulesShouldInternal(Function>, ArchRule> createRule) { + this.createRule = createRule; + } + + @Override + public ModulesRule respectTheirAllowedDependencies(DescribedPredicate> allowedDependencyPredicate, ModuleDependencyScope dependencyScope) { + return new ModulesRuleInternal<>( + createRule, + relevantClassDependencyPredicate -> new RespectTheirAllowedDependenciesCondition<>(allowedDependencyPredicate.forSubtype(), dependencyScope, relevantClassDependencyPredicate) + ); + } + + @Override + public ModulesRule respectTheirAllowedDependencies(AllowedModuleDependencies allowedDependencies, ModuleDependencyScope dependencyScope) { + return respectTheirAllowedDependencies( + allowedDependencies.asPredicate(), + dependencyScope + ); + } + + @Override + public ModulesRule onlyDependOnEachOtherThroughClassesThat(DescribedPredicate predicate) { + return new ModulesRuleInternal<>( + createRule, + relevantClassDependencyPredicate -> new ArchCondition>("only depend on each other through classes that " + predicate.getDescription()) { + @Override + public void check(ArchModule module, ConditionEvents events) { + module.getModuleDependenciesFromSelf().stream() + .flatMap(moduleDependency -> moduleDependency.toClassDependencies().stream()) + .filter(relevantClassDependencyPredicate) + .filter(classDependency -> !predicate.test(classDependency.getTargetClass())) + .forEach(classDependency -> events.add(SimpleConditionEvent.violated(classDependency, classDependency.getDescription()))); + } + } + ); + } + + @Override + public ClassesThat> onlyDependOnEachOtherThroughClassesThat() { + return new ClassesThatInternal<>(this::onlyDependOnEachOtherThroughClassesThat); + } + + @Override + public ModulesRule beFreeOfCycles() { + return new ModulesRuleInternal<>( + createRule, + relevantClassDependencyPredicate -> CycleArchCondition.>builder() + .retrieveClassesBy(Function.identity()) + .retrieveDescriptionBy(ArchModule::getName) + .retrieveOutgoingDependenciesBy(ArchModule::getClassDependenciesFromSelf) + .onlyConsiderDependencies(relevantClassDependencyPredicate) + .build() + ); + } + + static class ModulesByAnnotationShouldInternal extends ModulesShouldInternal> implements ModulesByAnnotationShould { + + ModulesByAnnotationShouldInternal(Function>>, ArchRule> createRule) { + super(createRule); + } + + @Override + public ModulesByAnnotationRule respectTheirAllowedDependenciesDeclaredIn(String annotationPropertyName, ModuleDependencyScope dependencyScope) { + return new ModulesByAnnotationRuleInternal<>( + respectTheirAllowedDependencies( + DescribedPredicate.describe( + "declared in '" + annotationPropertyName + "'", + moduleDependency -> { + Set allowedDependencies = getAllowedDependencies(moduleDependency.getOrigin().getDescriptor().getAnnotation(), annotationPropertyName); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }), + dependencyScope) + ); + } + + @Override + public ModulesByAnnotationRule onlyDependOnEachOtherThroughPackagesDeclaredIn(String annotationPropertyName) { + return new ModulesByAnnotationRuleInternal<>(new ModulesRuleInternal<>( + createRule, + relevantClassDependencyPredicate -> new ArchCondition>>( + String.format("only depend on each other through packages declared in '%s'", annotationPropertyName) + ) { + @Override + public void check(ArchModule> module, ConditionEvents events) { + // note that while this would be simpler to write via getClassDependenciesToSelf() we don't go this way because resolving + // reverse dependencies is more expensive. So as a library function it makes sense to choose the more performant way instead. + module.getModuleDependenciesFromSelf().forEach(moduleDependency -> { + ANNOTATION descriptor = moduleDependency.getTarget().getDescriptor().getAnnotation(); + String[] apiPackageIdentifiers = getStringArrayAnnotationProperty(descriptor, annotationPropertyName); + Predicate predicate = resideInAnyPackage(apiPackageIdentifiers); + + moduleDependency.toClassDependencies().stream() + .filter(relevantClassDependencyPredicate) + .filter(classDependency -> !predicate.test(classDependency.getTargetClass())) + .forEach(classDependency -> events.add(SimpleConditionEvent.violated(classDependency, classDependency.getDescription()))); + }); + } + } + )); + } + + private Set getAllowedDependencies(Annotation annotation, String annotationPropertyName) { + String[] allowedDependencies = getStringArrayAnnotationProperty(annotation, annotationPropertyName); + return ImmutableSet.copyOf(allowedDependencies); + } + + private String[] getStringArrayAnnotationProperty(Annotation annotation, String annotationPropertyName) { + Object value = getAnnotationProperty(annotation, annotationPropertyName); + try { + return (String[]) value; + } catch (ClassCastException e) { + String message = String.format("Property @%s.%s() must be of type String[]", annotation.annotationType().getSimpleName(), annotationPropertyName); + throw new IllegalArgumentException(message, e); + } + } + + private static Object getAnnotationProperty(Annotation annotation, String annotationPropertyName) { + try { + return annotation.annotationType().getMethod(annotationPropertyName).invoke(annotation); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + String message = String.format("Could not invoke @%s.%s()", annotation.annotationType().getSimpleName(), annotationPropertyName); + throw new IllegalArgumentException(message, e); + } + } + + private static class ModulesByAnnotationRuleInternal implements ModulesByAnnotationRule { + private final ModulesRule> delegate; + + ModulesByAnnotationRuleInternal(ModulesRule> delegate) { + this.delegate = delegate; + } + + @Override + public ModulesByAnnotationShouldInternal andShould() { + return new ModulesByAnnotationShouldInternal<>(this::andShould); + } + + @Override + public ArchRule andShould(ArchCondition>> condition) { + return delegate.andShould(condition); + } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + + @Override + public void check(JavaClasses classes) { + delegate.check(classes); + } + + @Override + public EvaluationResult evaluate(JavaClasses classes) { + return delegate.evaluate(classes); + } + + @Override + public ModulesByAnnotationRule as(String newDescription) { + return new ModulesByAnnotationRuleInternal<>(delegate.as(newDescription)); + } + + @Override + public ModulesByAnnotationRule because(String reason) { + return new ModulesByAnnotationRuleInternal<>(delegate.because(reason)); + } + + @Override + public ModulesByAnnotationRule allowEmptyShould(boolean allowEmptyShould) { + return new ModulesByAnnotationRuleInternal<>(delegate.allowEmptyShould(allowEmptyShould)); + } + + @Override + public ModulesByAnnotationRule ignoreDependency(Class origin, Class target) { + return new ModulesByAnnotationRuleInternal<>(delegate.ignoreDependency(origin, target)); + } + + @Override + public ModulesByAnnotationRule ignoreDependency(String originFullyQualifiedClassName, String targetFullyQualifiedClassName) { + return new ModulesByAnnotationRuleInternal<>(delegate.ignoreDependency(originFullyQualifiedClassName, targetFullyQualifiedClassName)); + } + + @Override + public ModulesByAnnotationRule ignoreDependency(Predicate originPredicate, Predicate targetPredicate) { + return new ModulesByAnnotationRuleInternal<>(delegate.ignoreDependency(originPredicate, targetPredicate)); + } + + @Override + public ModulesByAnnotationRule ignoreDependency(Predicate dependencyPredicate) { + return new ModulesByAnnotationRuleInternal<>(delegate.ignoreDependency(dependencyPredicate)); + } + } + } + + private static class RespectTheirAllowedDependenciesCondition extends ArchCondition> { + private final DescribedPredicate> allowedModuleDependencyPredicate; + private final ModuleDependencyScope dependencyScope; + private final Predicate relevantClassDependencyPredicate; + private Collection> allModules; + + RespectTheirAllowedDependenciesCondition( + DescribedPredicate> allowedModuleDependencyPredicate, + ModuleDependencyScope dependencyScope, + Predicate relevantClassDependencyPredicate + ) { + super("respect their allowed dependencies %s %s", allowedModuleDependencyPredicate.getDescription(), dependencyScope.getDescription()); + this.allowedModuleDependencyPredicate = allowedModuleDependencyPredicate; + this.dependencyScope = dependencyScope; + this.relevantClassDependencyPredicate = relevantClassDependencyPredicate; + } + + @Override + public void init(Collection> allModules) { + this.allModules = allModules; + } + + @Override + public void check(ArchModule module, ConditionEvents events) { + Set> actualDependencies = module.getModuleDependenciesFromSelf(); + + actualDependencies.stream() + .filter(it -> !allowedModuleDependencyPredicate.test(it)) + .filter(it -> it.toClassDependencies().stream().anyMatch(relevantClassDependencyPredicate)) + .forEach(it -> events.add(violated(it, it.getDescription()))); + + module.getUndefinedDependencies().stream() + .filter(dependencyScope.asPredicate(allModules)) + .filter(relevantClassDependencyPredicate) + .forEach(it -> events.add(violated(it, "Dependency not contained in any module: " + it.getDescription()))); + } + } + + private static class ModulesRuleInternal implements ModulesRule { + private final Function>, ArchRule> createRule; + private final Function modifyRule; + private final Function, ArchCondition>> createCondition; + private final Predicate relevantClassDependencyPredicate; + + ModulesRuleInternal( + Function>, ArchRule> createRule, + Function, ArchCondition>> createCondition + ) { + this(createRule, createCondition, x -> x, __ -> true); + } + + private ModulesRuleInternal( + Function>, ArchRule> createRule, + Function, ArchCondition>> createCondition, + Function modifyRule, + Predicate relevantClassDependencyPredicate + ) { + this.createRule = createRule; + this.createCondition = createCondition; + this.modifyRule = modifyRule; + this.relevantClassDependencyPredicate = relevantClassDependencyPredicate; + } + + @Override + public String getDescription() { + return createRule().getDescription(); + } + + @Override + public void check(JavaClasses classes) { + createRule().check(classes); + } + + @Override + public EvaluationResult evaluate(JavaClasses classes) { + return createRule().evaluate(classes); + } + + @Override + public ModulesShould andShould() { + return new ModulesShouldInternal<>(this::andShould); + } + + @Override + public ArchRule andShould(ArchCondition> condition) { + return createRule(createCondition().and(condition.as("should " + condition.getDescription()))); + } + + @Override + public ModulesRule as(String newDescription) { + return new ModulesRuleInternal<>( + createRule, + createCondition, + rule -> modifyRule.apply(rule).as(newDescription), + relevantClassDependencyPredicate); + } + + @Override + public ModulesRule because(String reason) { + return new ModulesRuleInternal<>( + createRule, + createCondition, + rule -> modifyRule.apply(rule).because(reason), + relevantClassDependencyPredicate); + } + + @Override + public ModulesRule allowEmptyShould(boolean allowEmptyShould) { + return new ModulesRuleInternal<>( + createRule, + createCondition, + rule -> modifyRule.apply(rule).allowEmptyShould(allowEmptyShould), + relevantClassDependencyPredicate); + } + + @Override + public ModulesRule ignoreDependency(Class origin, Class target) { + return ignoreDependency(dependency(origin, target)); + } + + @Override + public ModulesRule ignoreDependency(String originFullyQualifiedClassName, String targetFullyQualifiedClassName) { + return ignoreDependency(dependency(originFullyQualifiedClassName, targetFullyQualifiedClassName)); + } + + @Override + public ModulesRule ignoreDependency(Predicate originPredicate, Predicate targetPredicate) { + return ignoreDependency(dependency -> originPredicate.test(dependency.getOriginClass()) && targetPredicate.test(dependency.getTargetClass())); + } + + @Override + public ModulesRule ignoreDependency(Predicate dependencyPredicate) { + return new ModulesRuleInternal<>( + createRule, + createCondition, + modifyRule, + dependency -> this.relevantClassDependencyPredicate.test(dependency) && !dependencyPredicate.test(dependency)); + } + + private ArchRule createRule() { + return createRule(createCondition()); + } + + private ArchCondition> createCondition() { + return createCondition.apply(relevantClassDependencyPredicate); + } + + private ArchRule createRule(ArchCondition> condition) { + return modifyRule.apply(createRule.apply(condition)); + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesTransformer.java b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesTransformer.java new file mode 100644 index 0000000000..0a4f36bb18 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/modules/syntax/ModulesTransformer.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014-2023 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.Collection; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.tngtech.archunit.base.DescribedIterable; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.lang.ClassesTransformer; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ArchModules; + +import static java.util.stream.Collectors.toList; + +class ModulesTransformer implements ClassesTransformer> { + private final Function> transformFunction; + private final Predicate> predicate; + private final String description; + + ModulesTransformer(Function> transformFunction) { + this(transformFunction, __ -> true, "modules"); + } + + private ModulesTransformer( + Function> transformFunction, + Predicate> predicate, + String description + ) { + this.transformFunction = transformFunction; + this.predicate = predicate; + this.description = description; + } + + @Override + public DescribedIterable> transform(JavaClasses classes) { + Collection> modules = transformFunction.apply(classes).stream().filter(predicate).collect(toList()); + return DescribedIterable.From.iterable(modules, description); + } + + @Override + public ModulesTransformer that(DescribedPredicate> predicate) { + return new ModulesTransformer<>( + transformFunction, + predicate.forSubtype(), + description + " that " + predicate.getDescription() + ); + } + + ModulesTransformer and(DescribedPredicate> predicate) { + return new ModulesTransformer<>( + transformFunction, + x -> this.predicate.test(x) && predicate.test(x), + description + " and " + predicate.getDescription() + ); + } + + ModulesTransformer or(DescribedPredicate> predicate) { + return new ModulesTransformer<>( + transformFunction, + x -> this.predicate.test(x) || predicate.test(x), + description + " or " + predicate.getDescription() + ); + } + + @Override + public ModulesTransformer as(String description) { + return new ModulesTransformer<>(transformFunction, predicate, description); + } + + @Override + public String getDescription() { + return description; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java index 18e510fa70..ef9d39dd2b 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldClassesThatTest.java @@ -1778,7 +1778,7 @@ public void transitivelyDependOnClassesThat_reports_all_transitive_dependencies( : noClassesShould.transitivelyDependOnClassesThat().belongToAnyOf(matchingTransitivelyDependentClasses); assertThatRule(rule).checking(classes) - .hasViolations(3) + .hasNumberOfViolations(3) .hasViolationMatching(String.format(".*<%s> transitively depends on <(?:%s|%s)> by \\[%s->.*\\] in .*", quote(testClass1.getName()), quote(level2TransitivelyDependentClass1.getName()), diff --git a/archunit/src/test/java/com/tngtech/archunit/library/GeneralCodingRulesTest.java b/archunit/src/test/java/com/tngtech/archunit/library/GeneralCodingRulesTest.java index 07a578e2d4..22ef0e6bf0 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/GeneralCodingRulesTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/GeneralCodingRulesTest.java @@ -70,7 +70,7 @@ public void should_pass_when_one_of_multiple_matching_test_classes_resides_in_th public void should_not_pass_when_none_of_multiple_matching_test_classes_resides_in_implementation_package() { assertThatRule(testClassesShouldResideInTheSamePackageAsImplementation()) .checking(new ClassFileImporter().importPackagesOf(ImplementationClassWithMultipleTestsNotMatchingImplementationClassPackage.class)) - .hasViolations(2) + .hasNumberOfViolations(2) .hasViolationWithStandardPattern( com.tngtech.archunit.library.testclasses.packages.incorrect.nodirmatching.wrongdir1.ImplementationClassWithMultipleTestsNotMatchingImplementationClassPackageTest.class, "does not reside in same package as implementation class <" @@ -97,7 +97,7 @@ void f() { } assertThatRule(ASSERTIONS_SHOULD_HAVE_DETAIL_MESSAGE) .checking(new ClassFileImporter().importClasses(InvalidAssertions.class)) - .hasViolations(2) + .hasNumberOfViolations(2) .hasViolationContaining("Method <%s.f(int)> calls constructor <%s.()>", InvalidAssertions.class.getName(), AssertionError.class.getName()) .hasViolationContaining("Method <%s.f()> calls constructor <%s.()>", @@ -163,7 +163,7 @@ void origin() { assertThatRule(DEPRECATED_API_SHOULD_NOT_BE_USED) .hasDescriptionContaining("no classes should access @Deprecated members or should depend on @Deprecated classes, because there should be a better alternative") .checking(new ClassFileImporter().importClasses(Origin.class, ClassWithDeprecatedMembers.class, DeprecatedClass.class)) - .hasViolations(10) + .hasNumberOfViolations(10) .hasViolationContaining("%s calls constructor <%s.%s>", violatingMethod, ClassWithDeprecatedMembers.class.getName(), innerClassConstructor) .hasViolationContaining("%s gets field <%s.target>", violatingMethod, ClassWithDeprecatedMembers.class.getName()) .hasViolationContaining("%s sets field <%s.target>", violatingMethod, ClassWithDeprecatedMembers.class.getName()) diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/CycleTest.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/CycleInternalTest.java similarity index 56% rename from archunit/src/test/java/com/tngtech/archunit/library/dependencies/CycleTest.java rename to archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/CycleInternalTest.java index aedaafb08f..e18cae158b 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/CycleTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/CycleInternalTest.java @@ -1,23 +1,23 @@ -package com.tngtech.archunit.library.dependencies; - -import org.junit.Test; +package com.tngtech.archunit.library.cycle_detection; import java.util.List; -import static com.tngtech.archunit.library.dependencies.GraphTest.randomNode; -import static com.tngtech.archunit.library.dependencies.GraphTest.stringEdge; +import org.junit.Test; + +import static com.tngtech.archunit.library.cycle_detection.GraphTest.randomNode; +import static com.tngtech.archunit.library.cycle_detection.GraphTest.stringEdge; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class CycleTest { +public class CycleInternalTest { @Test public void rejects_invalid_edges() { - List> edges = asList(stringEdge(randomNode(), randomNode()), stringEdge(randomNode(), randomNode())); + List> edges = asList(stringEdge(randomNode(), randomNode()), stringEdge(randomNode(), randomNode())); assertThatThrownBy( - () -> new Cycle<>(edges) + () -> new CycleInternal<>(edges) ) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Edges are not connected"); @@ -25,9 +25,9 @@ public void rejects_invalid_edges() { @Test public void rejects_single_edge() { - List> edges = singletonList(stringEdge(randomNode(), randomNode())); + List> edges = singletonList(stringEdge(randomNode(), randomNode())); assertThatThrownBy( - () -> new Cycle<>(edges) + () -> new CycleInternal<>(edges) ) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("do not form a cycle"); @@ -37,7 +37,7 @@ public void rejects_single_edge() { public void minimal_nontrivial_cycle() { String nodeA = "Node-A"; String nodeB = "Node-B"; - Cycle cycle = new Cycle<>(asList(stringEdge(nodeA, nodeB), stringEdge(nodeB, nodeA))); + CycleInternal> cycle = new CycleInternal<>(asList(stringEdge(nodeA, nodeB), stringEdge(nodeB, nodeA))); assertThat(cycle.getEdges()).hasSize(2); } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/CyclesAssertion.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/CyclesAssertion.java new file mode 100644 index 0000000000..6fd6677383 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/CyclesAssertion.java @@ -0,0 +1,53 @@ +package com.tngtech.archunit.library.cycle_detection; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import org.assertj.core.api.AbstractObjectAssert; + +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("UnusedReturnValue") +class CyclesAssertion extends AbstractObjectAssert>> { + + protected CyclesAssertion(Collection> cycles) { + super(cycles, CyclesAssertion.class); + } + + static CyclesAssertion assertThatCycles(Iterable> cycles) { + return new CyclesAssertion(ImmutableList.copyOf(cycles)); + } + + CyclesAssertion hasSize(int size) { + assertThat(actual).as(descriptionText()).hasSize(size); + return this; + } + + CyclesAssertion containsOnly(Cycle... cycles) { + hasSize(cycles.length); + + Set>> thisOriginsAndTargets = actual.stream().map(it -> toOriginsAndTargets(it.getEdges())).collect(toSet()); + Set>> otherOriginsAndTargets = Arrays.stream(cycles).map(it -> toOriginsAndTargets(it.getEdges())).collect(toSet()); + assertThat(thisOriginsAndTargets).isEqualTo(otherOriginsAndTargets); + + return this; + } + + private Set> toOriginsAndTargets(List> edges) { + return edges.stream().map(it -> ImmutableList.of(it.getOrigin(), it.getTarget())).collect(toSet()); + } + + CyclesAssertion isEmpty() { + assertThat(actual).as(descriptionText()).isEmpty(); + return this; + } + + CyclesAssertion isNotEmpty() { + assertThat(actual).as(descriptionText()).isNotEmpty(); + return this; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/EdgeTest.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/EdgeTest.java new file mode 100644 index 0000000000..c235cba47b --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/EdgeTest.java @@ -0,0 +1,28 @@ +package com.tngtech.archunit.library.cycle_detection; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EdgeTest { + private final Object from = new Object(); + private final Object to = new Object(); + + @Test + public void edges_are_equal_iff_from_and_to_are_equal() { + + assertThat(Edge.create(from, to)).isEqualTo(Edge.create(from, to)); + assertThat(Edge.create(from, to)).isNotEqualTo(Edge.create(new Object(), to)); + assertThat(Edge.create(from, to)).isNotEqualTo(Edge.create(from, new Object())); + + Edge equalWithAttachment = Edge.create(from, to); + assertThat(Edge.create(from, to)).isEqualTo(equalWithAttachment); + } + + @Test + public void hashCode_of_two_equal_edges_is_equal() { + Edge equalEdge = Edge.create(from, to); + assertThat(Edge.create(from, to).hashCode()).isEqualTo(equalEdge.hashCode()); + } + +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/GraphTest.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/GraphTest.java similarity index 59% rename from archunit/src/test/java/com/tngtech/archunit/library/dependencies/GraphTest.java rename to archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/GraphTest.java index 3e7f8ee652..3f707f5230 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/GraphTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/GraphTest.java @@ -1,5 +1,6 @@ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Random; @@ -10,16 +11,16 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; import com.tngtech.archunit.ArchConfiguration; -import com.tngtech.archunit.library.dependencies.Graph.Cycles; import org.junit.Test; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.DiscreteDomain.integers; +import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Sets.cartesianProduct; -import static com.tngtech.archunit.library.dependencies.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; +import static com.tngtech.archunit.library.cycle_detection.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; +import static com.tngtech.archunit.library.cycle_detection.CyclesAssertion.assertThatCycles; import static java.util.Arrays.asList; -import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toSet; @@ -30,16 +31,16 @@ public class GraphTest { @Test public void graph_without_cycles() { - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); graph.addNodes(asList(randomNode(), randomNode(), randomNode())); - assertThat(graph.findCycles()).isEmpty(); + assertThatCycles(graph.findCycles()).isEmpty(); } @Test public void three_node_cycle_is_detected() { - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); String nodeA = "Node-A"; String nodeB = "Node-B"; @@ -47,7 +48,7 @@ public void three_node_cycle_is_detected() { graph.addNodes(asList(nodeA, nodeB, nodeC)); graph.addEdges(ImmutableSet.of(stringEdge(nodeA, nodeB), stringEdge(nodeB, nodeC), stringEdge(nodeC, nodeA))); - Cycle cycle = getOnlyElement(graph.findCycles()); + Cycle> cycle = getOnlyElement(graph.findCycles()); assertThat(cycle.getEdges()).hasSize(3); assertEdgeExists(cycle, nodeA, nodeB); @@ -57,7 +58,7 @@ public void three_node_cycle_is_detected() { @Test public void sub_cycle_of_three_node_graph_is_detected() { - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); String nodeA = "Node-A"; String nodeB = "Node-B"; @@ -65,7 +66,7 @@ public void sub_cycle_of_three_node_graph_is_detected() { graph.addNodes(asList(nodeA, nodeB, nodeC)); graph.addEdges(ImmutableSet.of(stringEdge(nodeB, nodeA), stringEdge(nodeA, nodeB))); - Cycle cycle = getOnlyElement(graph.findCycles()); + Cycle> cycle = getOnlyElement(graph.findCycles()); assertThat(cycle.getEdges()).hasSize(2); assertEdgeExists(cycle, nodeA, nodeB); assertEdgeExists(cycle, nodeB, nodeA); @@ -73,7 +74,7 @@ public void sub_cycle_of_three_node_graph_is_detected() { @Test public void nested_cycles_are_detected() { - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); String nodeA = "Node-A"; String nodeB = "Node-B"; @@ -81,46 +82,46 @@ public void nested_cycles_are_detected() { graph.addNodes(asList(nodeA, nodeB, nodeC)); graph.addEdges(ImmutableSet.of(stringEdge(nodeB, nodeA), stringEdge(nodeA, nodeB), stringEdge(nodeC, nodeA), stringEdge(nodeB, nodeC))); - assertThat(graph.findCycles()).hasSize(2); + assertThatCycles(graph.findCycles()).hasSize(2); } @Test public void multiple_cycles_are_detected() { - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); - Cycle threeElements = randomCycle(3); - Cycle fourElements = randomCycle(4); - Cycle fiveElements = randomCycle(5); + Cycle> threeElements = randomCycle(3); + Cycle> fourElements = randomCycle(4); + Cycle> fiveElements = randomCycle(5); addCycles(graph, threeElements, fourElements, fiveElements); addCrossLink(graph, threeElements, fourElements); addCrossLink(graph, fourElements, fiveElements); - Collection> cycles = graph.findCycles(); + Collection>> cycles = graph.findCycles(); - assertThat(cycles).containsOnly(threeElements, fourElements, fiveElements); + assertThatCycles(cycles).containsOnly(threeElements, fourElements, fiveElements); } @Test public void double_linked_three_node_cycle_results_in_five_cycles() { - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); - Cycle threeElements = randomCycle(3); + Cycle> threeElements = randomCycle(3); addCycles(graph, threeElements); - for (Edge edge : threeElements.getEdges()) { - graph.addEdges(singleEdge(edge.getTo(), edge.getFrom())); + for (Edge edge : threeElements.getEdges()) { + graph.addEdges(singleEdge(edge.getTarget(), edge.getOrigin())); } - assertThat(graph.findCycles()).hasSize(5); + assertThatCycles(graph.findCycles()).hasSize(5); } @Test public void complete_graph() { - Graph completeGraph = createCompleteGraph(3); - Iterable> cycles = completeGraph.findCycles(); + Graph> completeGraph = createCompleteGraph(3); + Collection>> cycles = completeGraph.findCycles(); - assertThat(cycles).containsOnly(createCycle(ImmutableList.of(0, 1, 2, 0)), + assertThatCycles(cycles).containsOnly(createCycle(ImmutableList.of(0, 1, 2, 0)), createCycle(ImmutableList.of(0, 2, 1, 0)), createCycle(ImmutableList.of(0, 1, 0)), createCycle(ImmutableList.of(1, 2, 1)), @@ -130,7 +131,7 @@ public void complete_graph() { @Test public void graph_which_causes_error_when_dependently_blocked_nodes_are_not_cleared_after_unblocking() { ImmutableSet nodes = ImmutableSet.of(0, 1, 2, 3, 4, 5); - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); graph.addNodes(nodes); graph.addEdges(ImmutableSet.of( @@ -145,26 +146,25 @@ public void graph_which_causes_error_when_dependently_blocked_nodes_are_not_clea newEdge(5, 2) )); - assertThat(graph.findCycles()).isNotEmpty(); + assertThatCycles(graph.findCycles()).isNotEmpty(); } // This test covers some edge cases, e.g. if too many nodes stay blocked @Test public void finds_cycles_in_real_life_graph() { - Graph graph = RealLifeGraph.get(); + Graph> graph = RealLifeGraph.get(); int expectedNumberOfCycles = 10000; ArchConfiguration.get().setProperty(MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME, String.valueOf(expectedNumberOfCycles)); - Cycles cycles = graph.findCycles(); + Cycles> cycles = graph.findCycles(); - assertThat(cycles).hasSize(expectedNumberOfCycles); + assertThatCycles(cycles).hasSize(expectedNumberOfCycles); assertThat(cycles.maxNumberOfCyclesReached()).as("maximum number of cycles reached").isTrue(); } - @SuppressWarnings("unchecked") - private Graph createCompleteGraph(int n) { + private Graph> createCompleteGraph(int n) { ContiguousSet integers = ContiguousSet.create(Range.closedOpen(0, n), integers()); - Graph graph = new Graph<>(); + Graph> graph = new Graph<>(); graph.addNodes(integers); graph.addEdges(cartesianProduct(integers, integers).stream() .filter(input -> !input.get(0).equals(input.get(1))) @@ -173,43 +173,44 @@ private Graph createCompleteGraph(int n) { return graph; } - private Cycle createCycle(List numbers) { - ImmutableList.Builder> builder = ImmutableList.builder(); + private Cycle> createCycle(List numbers) { + ImmutableList.Builder> builder = ImmutableList.builder(); for (int i = 0; i < numbers.size() - 1; i++) { builder.add(integerEdge(numbers.get(i), numbers.get(i + 1))); } - return new Cycle<>(builder.build()); + return new CycleInternal<>(builder.build()); } - private Cycle randomCycle(int numberOfNodes) { + private Cycle> randomCycle(int numberOfNodes) { checkArgument(numberOfNodes > 1, "A cycle can't be formed by less than 2 nodes"); - Path path = new Path<>(singleEdgeList(randomNode(), randomNode())); + List> path = new ArrayList<>(singleEdgeList(randomNode(), randomNode())); for (int i = 0; i < numberOfNodes - 2; i++) { - path.append(stringEdge(path.getEnd(), randomNode())); + path.add(stringEdge(getLast(path).getTarget(), randomNode())); } - return new Cycle<>(path.append(stringEdge(path.getEnd(), path.getStart()))); + path.add(stringEdge(getLast(path).getTarget(), path.get(0).getOrigin())); + return new CycleInternal<>(path); } @SafeVarargs - private final void addCycles(Graph graph, Cycle... cycles) { - for (Cycle cycle : cycles) { - for (Edge edge : cycle.getEdges()) { - graph.addNodes(asList(edge.getFrom(), edge.getTo())); + private final void addCycles(Graph> graph, Cycle>... cycles) { + for (Cycle> cycle : cycles) { + for (Edge edge : cycle.getEdges()) { + graph.addNodes(asList(edge.getOrigin(), edge.getTarget())); } graph.addEdges(cycle.getEdges()); } } - private void addCrossLink(Graph graph, Cycle first, Cycle second) { + private void addCrossLink(Graph> graph, Cycle> first, Cycle> second) { Random rand = new Random(); - String origin = first.getEdges().get(rand.nextInt(first.getEdges().size())).getFrom(); - String target = second.getEdges().get(rand.nextInt(second.getEdges().size())).getFrom(); + String origin = first.getEdges().get(rand.nextInt(first.getEdges().size())).getOrigin(); + String target = second.getEdges().get(rand.nextInt(second.getEdges().size())).getOrigin(); graph.addEdges(singleEdge(origin, target)); } - private static void assertEdgeExists(Cycle cycle, Object from, Object to) { - for (Edge edge : cycle.getEdges()) { - if (edge.getFrom().equals(from) && edge.getTo().equals(to)) { + private static void assertEdgeExists(Cycle cycle, Object from, Object to) { + for (Edge edge : cycle.getEdges()) { + if (edge.getOrigin().equals(from) && edge.getTarget().equals(to)) { return; } } @@ -220,23 +221,23 @@ static String randomNode() { return "" + random.nextLong() + System.nanoTime(); } - static Edge stringEdge(String nodeA, String nodeB) { + static Edge stringEdge(String nodeA, String nodeB) { return newEdge(nodeA, nodeB); } - private Edge integerEdge(Integer origin, Integer target) { + private Edge integerEdge(Integer origin, Integer target) { return newEdge(origin, target); } - static List> singleEdgeList(String from, String to) { + static List> singleEdgeList(String from, String to) { return singletonList(stringEdge(from, to)); } - static Set> singleEdge(String from, String to) { + static Set> singleEdge(String from, String to) { return singleton(stringEdge(from, to)); } - static Edge newEdge(NODE from, NODE to) { - return new Edge<>(from, to, emptySet()); + static Edge newEdge(NODE from, NODE to) { + return Edge.create(from, to); } } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/PathTest.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/PathTest.java similarity index 56% rename from archunit/src/test/java/com/tngtech/archunit/library/dependencies/PathTest.java rename to archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/PathTest.java index f0edb52355..b4276ea35e 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/PathTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/PathTest.java @@ -1,11 +1,11 @@ -package com.tngtech.archunit.library.dependencies; - -import org.junit.Test; +package com.tngtech.archunit.library.cycle_detection; import java.util.List; -import static com.tngtech.archunit.library.dependencies.GraphTest.randomNode; -import static com.tngtech.archunit.library.dependencies.GraphTest.stringEdge; +import org.junit.Test; + +import static com.tngtech.archunit.library.cycle_detection.GraphTest.randomNode; +import static com.tngtech.archunit.library.cycle_detection.GraphTest.stringEdge; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -13,7 +13,7 @@ public class PathTest { @Test public void rejects_invalid_edges() { - List> edges = asList(stringEdge(randomNode(), randomNode()), stringEdge(randomNode(), randomNode())); + List> edges = asList(stringEdge(randomNode(), randomNode()), stringEdge(randomNode(), randomNode())); assertThatThrownBy( () -> new Path<>(edges) ) diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/PrimitiveDataTypesTest.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/PrimitiveDataTypesTest.java similarity index 84% rename from archunit/src/test/java/com/tngtech/archunit/library/dependencies/PrimitiveDataTypesTest.java rename to archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/PrimitiveDataTypesTest.java index 494b7f90a5..689cbac272 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/PrimitiveDataTypesTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/PrimitiveDataTypesTest.java @@ -1,6 +1,6 @@ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; -import com.tngtech.archunit.library.dependencies.PrimitiveDataTypes.IntStack; +import com.tngtech.archunit.library.cycle_detection.PrimitiveDataTypes.IntStack; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -28,4 +28,4 @@ public void conversion_to_array_gets_all_elements() { int[] expected = {1, 2, 4}; assertThat(intStack.asArray()).isEqualTo(expected); } -} \ No newline at end of file +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/RealLifeGraph.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/RealLifeGraph.java similarity index 87% rename from archunit/src/test/java/com/tngtech/archunit/library/dependencies/RealLifeGraph.java rename to archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/RealLifeGraph.java index a996418f5e..1cc2a83e4c 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/RealLifeGraph.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/RealLifeGraph.java @@ -1,4 +1,4 @@ -package com.tngtech.archunit.library.dependencies; +package com.tngtech.archunit.library.cycle_detection; import java.util.ArrayList; import java.util.Collection; @@ -11,7 +11,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; -import static com.tngtech.archunit.library.dependencies.GraphTest.newEdge; +import static com.tngtech.archunit.library.cycle_detection.GraphTest.newEdge; public class RealLifeGraph { // This graph has originally been taken from the toplevel packages of org.hibernate @@ -67,24 +67,24 @@ private static Multimap createEdges() { return result; } - private static final Graph graph = createGraphFrom(edgeTargetsByOrigin); + private static final Graph> graph = createGraphFrom(edgeTargetsByOrigin); - private static Graph createGraphFrom(Multimap edges) { - Graph result = new Graph<>(); + private static Graph> createGraphFrom(Multimap edges) { + Graph> result = new Graph<>(); addNodes(result, edges); addEdges(result, edges); return result; } - private static void addNodes(Graph result, Multimap edges) { + private static void addNodes(Graph> result, Multimap edges) { Set nodes = new HashSet<>(); nodes.addAll(edges.keySet()); nodes.addAll(edges.values()); result.addNodes(nodes); } - private static void addEdges(Graph result, Multimap targetNodesByOriginNodes) { - List> edges = new ArrayList<>(); + private static void addEdges(Graph> result, Multimap targetNodesByOriginNodes) { + List> edges = new ArrayList<>(); for (Map.Entry> targetNodesByOrigin : targetNodesByOriginNodes.asMap().entrySet()) { for (Integer target : targetNodesByOrigin.getValue()) { edges.add(newEdge(targetNodesByOrigin.getKey(), target)); @@ -93,7 +93,7 @@ private static void addEdges(Graph result, Multimap get() { + public static Graph> get() { return graph; } } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/rules/CycleRuleTestConfiguration.java b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/rules/CycleRuleTestConfiguration.java new file mode 100644 index 0000000000..fea79a6b20 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/cycle_detection/rules/CycleRuleTestConfiguration.java @@ -0,0 +1,6 @@ +package com.tngtech.archunit.library.cycle_detection.rules; + +public class CycleRuleTestConfiguration { + public static final String MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME = + CycleRuleConfiguration.MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/EdgeTest.java b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/EdgeTest.java deleted file mode 100644 index 61fb24ccdf..0000000000 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/EdgeTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.tngtech.archunit.library.dependencies; - -import org.junit.Test; - -import static java.util.Collections.emptySet; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; - -public class EdgeTest { - private final Object from = new Object(); - private final Object to = new Object(); - - @Test - public void edges_are_equal_iff_from_and_to_are_equal() { - - assertThat(new Edge<>(from, to, emptySet())).isEqualTo(new Edge<>(from, to, emptySet())); - assertThat(new Edge<>(from, to, emptySet())).isNotEqualTo(new Edge<>(new Object(), to, emptySet())); - assertThat(new Edge<>(from, to, emptySet())).isNotEqualTo(new Edge<>(from, new Object(), emptySet())); - - Edge equalWithAttachment = new Edge<>(from, to, singletonList(new Object())); - assertThat(new Edge<>(from, to, emptySet())).isEqualTo(equalWithAttachment); - } - - @Test - public void hashCode_of_two_equal_edges_is_equal() { - Edge equalEdge = new Edge<>(from, to, singletonList(new Object())); - assertThat(new Edge<>(from, to, emptySet()).hashCode()).isEqualTo(equalEdge.hashCode()); - } - -} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/RandomSlicesSyntaxTest.java b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/RandomSlicesSyntaxTest.java index aae2fac032..e9636eea11 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/RandomSlicesSyntaxTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/RandomSlicesSyntaxTest.java @@ -1,7 +1,6 @@ package com.tngtech.archunit.library.dependencies; import java.util.List; -import java.util.regex.Matcher; import java.util.regex.Pattern; import com.tngtech.archunit.library.dependencies.syntax.GivenSlices; @@ -45,30 +44,4 @@ public String toString() { return getClass().getSimpleName() + "{" + skipPattern + "}"; } } - - private static class ReplaceEverythingSoFar implements DescriptionReplacement { - private final Pattern pattern; - private final String replaceWith; - - ReplaceEverythingSoFar(String pattern, String replaceWith) { - this.pattern = Pattern.compile(pattern); - this.replaceWith = replaceWith; - } - - @Override - public boolean applyTo(String currentToken, List currentDescription) { - Matcher matcher = pattern.matcher(currentToken); - if (matcher.matches()) { - currentDescription.clear(); - currentDescription.add(matcher.replaceAll(replaceWith)); - return true; - } - return false; - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{/" + pattern + "/" + replaceWith + "/}"; - } - } } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRulePerformanceTest.java b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRulePerformanceTest.java index 381c6ec575..a960cac4f4 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRulePerformanceTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRulePerformanceTest.java @@ -11,7 +11,7 @@ import org.junit.Test; import org.junit.experimental.categories.Category; -import static com.tngtech.archunit.library.dependencies.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; +import static com.tngtech.archunit.library.cycle_detection.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; import static com.tngtech.archunit.library.dependencies.SliceRuleTest.getNumberOfCyclesInCompleteGraph; import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; import static org.assertj.core.api.Assertions.assertThat; diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRuleTest.java b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRuleTest.java index 1ace87cfae..c21a0f844f 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRuleTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceRuleTest.java @@ -21,8 +21,8 @@ import org.junit.runner.RunWith; import static com.google.common.math.IntMath.factorial; -import static com.tngtech.archunit.library.dependencies.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; -import static com.tngtech.archunit.library.dependencies.CycleConfiguration.MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME; +import static com.tngtech.archunit.library.cycle_detection.CycleConfiguration.MAX_NUMBER_OF_CYCLES_TO_DETECT_PROPERTY_NAME; +import static com.tngtech.archunit.library.cycle_detection.rules.CycleRuleTestConfiguration.MAX_NUMBER_OF_DEPENDENCIES_TO_SHOW_PER_EDGE_PROPERTY_NAME; import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; import static com.tngtech.java.junit.dataprovider.DataProviders.$; import static com.tngtech.java.junit.dataprovider.DataProviders.$$; diff --git a/archunit/src/test/java/com/tngtech/archunit/library/freeze/FreezingArchRuleTest.java b/archunit/src/test/java/com/tngtech/archunit/library/freeze/FreezingArchRuleTest.java index 1e6e0369c6..8c5e4f87d9 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/freeze/FreezingArchRuleTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/freeze/FreezingArchRuleTest.java @@ -248,7 +248,7 @@ public void fails_on_an_increased_violation_count_of_the_same_violation_compared assertThatRule(frozen) .checking(importClasses(getClass())) - .hasViolations(1) + .hasNumberOfViolations(1) .hasAnyViolationOf("violation", "equivalent one"); } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/ArchModuleTest.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/ArchModuleTest.java new file mode 100644 index 0000000000..7c00239d7b --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/ArchModuleTest.java @@ -0,0 +1,55 @@ +package com.tngtech.archunit.library.modules; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static com.tngtech.java.junit.dataprovider.DataProviders.$; +import static com.tngtech.java.junit.dataprovider.DataProviders.$$; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(DataProviderRunner.class) +public class ArchModuleTest { + + @Test + public void identifier_equals_hashcode_and_toString() { + ArchModule.Identifier original = ArchModule.Identifier.from("one", "two"); + ArchModule.Identifier equal = ArchModule.Identifier.from("one", "two"); + ArchModule.Identifier different = ArchModule.Identifier.from("one", "other"); + + assertThat(equal).isEqualTo(original); + assertThat(equal.hashCode()).isEqualTo(original.hashCode()); + assertThat(equal).isNotEqualTo(different); + } + + @Test + public void identifier_parts() { + ArchModule.Identifier identifier = ArchModule.Identifier.from("one", "two", "three"); + + assertThat(identifier).containsExactly("one", "two", "three"); + assertThat(identifier.getNumberOfParts()).as("number of parts").isEqualTo(3); + assertThat(identifier.getPart(1)).isEqualTo("one"); + assertThat(identifier.getPart(2)).isEqualTo("two"); + assertThat(identifier.getPart(3)).isEqualTo("three"); + } + + @DataProvider + public static Object[][] illegal_indices() { + return $$( + $(ArchModule.Identifier.from("one"), 0), + $(ArchModule.Identifier.from("one"), 2) + ); + } + + @Test + @UseDataProvider("illegal_indices") + public void rejects_index_out_of_range(ArchModule.Identifier identifier, int illegalIndex) { + assertThatThrownBy(() -> identifier.getPart(illegalIndex)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.valueOf(illegalIndex)) + .hasMessageContaining("out of bounds"); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/ArchModulesTest.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/ArchModulesTest.java new file mode 100644 index 0000000000..521d98c497 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/ArchModulesTest.java @@ -0,0 +1,567 @@ +package com.tngtech.archunit.library.modules; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import com.google.common.base.Splitter; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.library.modules.ArchModule.Identifier; +import com.tngtech.archunit.library.modules.testexamples.MyModule; +import com.tngtech.archunit.library.modules.testexamples.annotation_with_custom_name.MyModuleWithCustomName; +import com.tngtech.archunit.library.modules.testexamples.annotation_with_custom_name.module1.ModuleOneDescriptorCustomName; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.FirstClassInModule1; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.ModuleOneDescriptor; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.SecondClassInModule1; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.sub1.FirstClassInSubModule11; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.sub1.SecondClassInSubModule11; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.sub2.FirstClassInSubModule12; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.sub2.SecondClassInSubModule12; +import com.tngtech.archunit.library.modules.testexamples.valid.module2.FirstClassInModule2; +import com.tngtech.archunit.library.modules.testexamples.valid.module2.ModuleTwoDescriptor; +import com.tngtech.archunit.library.modules.testexamples.valid.module2.sub1.FirstClassInSubModule21; +import com.tngtech.archunit.testutil.assertion.DependenciesAssertion.ExpectedDependencies; +import org.assertj.core.api.AbstractObjectAssert; +import org.junit.Test; + +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.MoreCollectors.onlyElement; +import static com.tngtech.archunit.library.modules.ArchModulesTest.ModuleDependenciesAssertion.ExpectedModuleDependency.from; +import static com.tngtech.archunit.testutil.Assertions.assertThatDependencies; +import static com.tngtech.archunit.testutil.Assertions.assertThatTypes; +import static com.tngtech.archunit.testutil.assertion.DependenciesAssertion.from; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ArchModulesTest { + private final String validTestExamplePackage = getExamplePackage("valid"); + private final JavaClasses testExamples = new ClassFileImporter().importPackages(validTestExamplePackage); + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + public void partitions_modules_by_single_package_not_including_subpackages() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module1"); + + assertThatTypes(module).matchInAnyOrder( + ModuleOneDescriptor.class, + FirstClassInModule1.class, + SecondClassInModule1.class); + + assertThat(modules.tryGetByIdentifier("module1")).contains((ArchModule) module); + assertThat(modules.tryGetByIdentifier("absent")).isEmpty(); + } + + @Test + public void partitions_modules_by_single_package_each_including_subpackages() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*)..") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module1"); + + assertThatTypes(module).matchInAnyOrder( + ModuleOneDescriptor.class, + FirstClassInModule1.class, + SecondClassInModule1.class, + FirstClassInSubModule11.class, + SecondClassInSubModule11.class, + FirstClassInSubModule12.class, + SecondClassInSubModule12.class); + } + + @Test + public void partitions_modules_by_multiple_separate_packages() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module1", "sub1"); + + assertThatTypes(module).matchInAnyOrder( + FirstClassInSubModule11.class, + SecondClassInSubModule11.class); + + assertThat(modules.tryGetByIdentifier("module1")).isEmpty(); + assertThat(modules.tryGetByIdentifier("module1.sub")).isEmpty(); + assertThat(modules.tryGetByIdentifier("module1:sub")).isEmpty(); + } + + @Test + public void partitions_modules_by_multiple_unified_packages() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(**)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module1.sub1"); + + assertThatTypes(module).matchInAnyOrder( + FirstClassInSubModule11.class, + SecondClassInSubModule11.class); + + assertThat(modules.tryGetByIdentifier("module1", "sub1")).isEmpty(); + } + + @Test + public void names_modules_by_default() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .modularize(testExamples); + + assertThat(modules.getNames()).containsOnly( + "module1:sub1", + "module1:sub2", + "module2:sub1", + "module2:sub2"); + } + + @Test + public void allows_naming_modules() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .deriveNameFromPattern("MyModule [$1][${2}]") + .modularize(testExamples); + + assertThat(modules.getNames()).containsOnly( + "MyModule [module1][sub1]", + "MyModule [module1][sub2]", + "MyModule [module2][sub1]", + "MyModule [module2][sub2]"); + } + + @Test + public void rejects_multiple_modules_with_same_name() { + String duplicateName = "alwaysSame"; + + assertThatThrownBy( + () -> ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .deriveNameFromPattern(duplicateName) + .modularize(testExamples) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Found multiple modules with the same name: [" + duplicateName + "]"); + } + + @Test + public void supports_joined_identifier_when_naming_modules() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .deriveNameFromPattern("MyModule [$@]") + .modularize(testExamples); + + assertThat(modules.getNames()).containsOnly( + "MyModule [module1:sub1]", + "MyModule [module1:sub2]", + "MyModule [module2:sub1]", + "MyModule [module2:sub2]"); + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + public void allows_retrieving_modules_by_name() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .deriveNameFromPattern("MyModule [$1][$2]") + .modularize(testExamples); + + ArchModule module = modules.getByName("MyModule [module1][sub1]"); + + assertThatTypes(module).matchInAnyOrder( + FirstClassInSubModule11.class, + SecondClassInSubModule11.class); + + assertThat(modules.tryGetByName("MyModule [module1][sub1]")).contains((ArchModule) module); + assertThat(modules.tryGetByName("absent")).isEmpty(); + } + + @Test + public void allows_defining_modules_by_function() { + ArchModules modules = ArchModules + .defineBy(javaClass -> { + String suffix = javaClass.getPackageName().replace(validTestExamplePackage, ""); + List parts = Splitter.on(".").omitEmptyStrings().splitToList(suffix); + return parts.size() > 1 ? Identifier.from(parts.subList(0, 2)) : Identifier.ignore(); + }) + .deriveNameFromPattern("Any $1->$2") + .modularize(testExamples); + + assertThat(modules.getNames()).containsOnly( + "Any module1->sub1", + "Any module1->sub2", + "Any module2->sub1", + "Any module2->sub2"); + } + + @Test + public void allows_defining_modules_by_root_classes() { + ArchModules modules = ArchModules + .defineByRootClasses(javaClass -> javaClass.getSimpleName().endsWith("Descriptor")) + .modularize(testExamples); + + assertThat(modules.getNames()).containsOnly( + ModuleOneDescriptor.class.getPackage().getName(), + ModuleTwoDescriptor.class.getPackage().getName() + ); + + ArchModule module = modules.getByIdentifier(ModuleOneDescriptor.class.getPackage().getName()); + + assertThatTypes(module).matchInAnyOrder( + ModuleOneDescriptor.class, + FirstClassInModule1.class, + SecondClassInModule1.class, + FirstClassInSubModule11.class, + SecondClassInSubModule11.class, + FirstClassInSubModule12.class, + SecondClassInSubModule12.class); + } + + @Test + public void rejects_overlapping_modules_by_root_classes() { + JavaClasses invalidExamples = new ClassFileImporter().importPackages(getExamplePackage("invalid")); + + assertThatThrownBy( + () -> ArchModules + .defineByRootClasses(javaClass -> javaClass.getSimpleName().endsWith("Descriptor")) + .modularize(invalidExamples) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("modules would overlap") + .hasMessageContaining( + com.tngtech.archunit.library.modules.testexamples.invalid.overlapping_root_classes.ModuleOneDescriptor.class.getPackage().getName()) + .hasMessageContaining( + com.tngtech.archunit.library.modules.testexamples.invalid.overlapping_root_classes.child.ModuleTwoDescriptor.class.getPackage().getName()); + } + + @Test + public void allows_defining_modules_by_annotations() { + ArchModules> modules = ArchModules + .defineByAnnotation(MyModule.class) + .modularize(testExamples); + + ArchModule> module = modules.getByIdentifier(ModuleOneDescriptor.class.getPackage().getName()); + + String expectedModuleName = ModuleOneDescriptor.class.getAnnotation(MyModule.class).name(); + assertThat(module.getName()).isEqualTo(expectedModuleName); + assertThat(module.getDescriptor().getAnnotation().name()).isEqualTo(expectedModuleName); + + assertThatTypes(module).matchInAnyOrder( + ModuleOneDescriptor.class, + FirstClassInModule1.class, + SecondClassInModule1.class, + FirstClassInSubModule11.class, + SecondClassInSubModule11.class, + FirstClassInSubModule12.class, + SecondClassInSubModule12.class); + } + + @Test + public void rejects_defining_modules_by_annotations_when_name_property_can_not_be_derived() { + JavaClasses classes = new ClassFileImporter().importPackages(getExamplePackage("annotation_with_custom_name")); + + assertInvalidAnnotationDefinitionByDefaultNameProperty(MyModuleWithCustomName.class, classes); + } + + @Test + public void rejects_defining_modules_by_annotations_when_name_property_has_incompatible_type() { + @AnnotationWithIncompatibleNameProperty(name = 42) + class RootClassWithIncompatibleAnnotation { + } + JavaClasses classes = new ClassFileImporter().importClasses(AnnotationWithIncompatibleNameProperty.class, RootClassWithIncompatibleAnnotation.class); + + assertInvalidAnnotationDefinitionByDefaultNameProperty(AnnotationWithIncompatibleNameProperty.class, classes); + } + + private static void assertInvalidAnnotationDefinitionByDefaultNameProperty(Class annotationType, JavaClasses classes) { + assertThatThrownBy( + () -> ArchModules + .defineByAnnotation(annotationType) + .modularize(classes) + ).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("@" + annotationType.getSimpleName() + ".name()") + .hasMessageContaining("Supplied annotation must provide a method 'String name()'") + .hasMessageContaining("defineByAnnotation(annotationType, nameFunction)"); + } + + @Test + public void allows_defining_modules_by_annotations_with_customized_name_property() { + JavaClasses invalidExamples = new ClassFileImporter().importPackages(getExamplePackage("annotation_with_custom_name")); + + ArchModules> modules = ArchModules + .defineByAnnotation(MyModuleWithCustomName.class, MyModuleWithCustomName::customName) + .modularize(invalidExamples); + + ArchModule module = modules.getByIdentifier(ModuleOneDescriptorCustomName.class.getPackage().getName()); + + String expectedModuleName = ModuleOneDescriptorCustomName.class.getAnnotation(MyModuleWithCustomName.class).customName(); + assertThat(module.getName()).isEqualTo(expectedModuleName); + } + + @Test + public void rejects_overlapping_modules_by_annotations() { + JavaClasses invalidExamples = new ClassFileImporter().importPackages(getExamplePackage("invalid")); + + assertThatThrownBy( + () -> ArchModules + .defineByAnnotation(MyModule.class) + .modularize(invalidExamples) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("modules would overlap") + .hasMessageContaining( + com.tngtech.archunit.library.modules.testexamples.invalid.overlapping_root_classes.ModuleOneDescriptor.class.getPackage().getName()) + .hasMessageContaining( + com.tngtech.archunit.library.modules.testexamples.invalid.overlapping_root_classes.child.ModuleTwoDescriptor.class.getPackage().getName()); + } + + @Test + public void allows_customizing_modules_defined_by_root_classes_from_all_classes_of_the_module() { + ArchModules modules = ArchModules + .defineByRootClasses(javaClass -> javaClass.getSimpleName().endsWith("Descriptor")) + .describeBy((__, containedClasses) -> { + JavaClass descriptorClass = containedClasses.stream() + .filter(it -> it.getSimpleName().endsWith("Descriptor")) + .collect(onlyElement()); + String name = getValue(descriptorClass.getField("name").reflect()); + return new TestModuleDescriptor(name, descriptorClass); + }) + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier(ModuleOneDescriptor.class.getPackage().getName()); + + assertThat(module.getName()).isEqualTo(ModuleOneDescriptor.name); + assertThat(module.getDescriptor().getDescriptorClass().getName()).isEqualTo(ModuleOneDescriptor.class.getName()); + } + + @Test + public void allows_customizing_modules_defined_by_root_classes_directly_from_root_class() { + ArchModules modules = ArchModules + .defineByRootClasses(javaClass -> javaClass.getSimpleName().endsWith("Descriptor")) + .describeModuleByRootClass((__, descriptorClass) -> { + String name = getValue(descriptorClass.getField("name").reflect()); + return new TestModuleDescriptor(name, descriptorClass); + }) + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier(ModuleOneDescriptor.class.getPackage().getName()); + + assertThat(module.getName()).isEqualTo(ModuleOneDescriptor.name); + assertThat(module.getDescriptor().getDescriptorClass().getName()).isEqualTo(ModuleOneDescriptor.class.getName()); + } + + @Test + public void provides_class_dependencies_from_self() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module1", "sub1"); + + assertThatDependencies(module.getClassDependenciesFromSelf()) + .containOnly(from(FirstClassInSubModule11.class).to(FirstClassInSubModule12.class) + .from(FirstClassInSubModule11.class).to(FirstClassInSubModule12[].class) + .from(FirstClassInSubModule11.class).to(SecondClassInSubModule12.class) + .from(FirstClassInSubModule11.class).to(SecondClassInSubModule12[].class) + .from(FirstClassInSubModule11.class).to(SecondClassInSubModule12[][].class) + .from(SecondClassInSubModule11.class).to(FirstClassInModule2.class) + .from(SecondClassInSubModule11.class).to(FirstClassInSubModule21.class) + .from(FirstClassInSubModule11.class).to(String.class) + .from(FirstClassInSubModule11.class).to(List.class) + .from(FirstClassInSubModule11.class).to(Object.class) + .from(SecondClassInSubModule11.class).to(Object.class) + .from(SecondClassInSubModule11.class).to(Collection.class) + ); + } + + @Test + public void provides_class_dependencies_to_self() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module2", "sub1"); + + assertThatDependencies(module.getClassDependenciesToSelf()) + .containOnly(from(SecondClassInSubModule11.class).to(FirstClassInSubModule21.class) + .from(FirstClassInSubModule12.class).to(FirstClassInSubModule21.class)); + } + + @Test + public void provides_module_dependencies_from_self() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module1", "sub1"); + + assertThatModuleDependencies(module.getModuleDependenciesFromSelf()) + .containOnlyModuleDependencies( + from("module1", "sub1").to("module1", "sub2") + .withClassDependencies( + from(FirstClassInSubModule11.class).to(FirstClassInSubModule12.class) + .from(FirstClassInSubModule11.class).to(FirstClassInSubModule12[].class) + .from(FirstClassInSubModule11.class).to(SecondClassInSubModule12.class) + .from(FirstClassInSubModule11.class).to(SecondClassInSubModule12[].class) + .from(FirstClassInSubModule11.class).to(SecondClassInSubModule12[][].class)), + from("module1", "sub1").to("module2", "sub1") + .withClassDependencies(from(SecondClassInSubModule11.class).to(FirstClassInSubModule21.class))); + } + + @Test + public void provides_module_dependencies_to_self() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module2", "sub1"); + + assertThatModuleDependencies(module.getModuleDependenciesToSelf()) + .containOnlyModuleDependencies( + from("module1", "sub1").to("module2", "sub1") + .withClassDependencies(from(SecondClassInSubModule11.class).to(FirstClassInSubModule21.class)), + from("module1", "sub2").to("module2", "sub1") + .withClassDependencies( + from(FirstClassInSubModule12.class).to(FirstClassInSubModule21.class) + .from(FirstClassInSubModule12.class).to(FirstClassInSubModule21[].class) + .from(FirstClassInSubModule12.class).to(FirstClassInSubModule21[][].class) + )); + } + + @Test + public void all_dependencies_not_covered_by_module_dependencies_are_considered_undefined() { + ArchModules modules = ArchModules + .defineByPackages(validTestExamplePackage + ".(*).(*)") + .modularize(testExamples); + + ArchModule module = modules.getByIdentifier("module1", "sub1"); + + assertThatDependencies(module.getUndefinedDependencies()) + .containOnly( + from(FirstClassInSubModule11.class).to(String.class) + .from(FirstClassInSubModule11.class).to(Object.class) + .from(FirstClassInSubModule11.class).to(List.class) + .from(SecondClassInSubModule11.class).to(Collection.class) + .from(SecondClassInSubModule11.class).to(FirstClassInModule2.class) + .from(SecondClassInSubModule11.class).to(Object.class)); + } + + @SuppressWarnings("unchecked") + private T getValue(Field staticField) { + try { + return (T) staticField.get(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private String getExamplePackage(String subpackageName) { + return getClass().getPackage().getName() + ".testexamples." + subpackageName; + } + + private static ModuleDependenciesAssertion assertThatModuleDependencies(Collection> dependencies) { + return new ModuleDependenciesAssertion(dependencies); + } + + static class ModuleDependenciesAssertion extends + AbstractObjectAssert>> { + + ModuleDependenciesAssertion(Collection> dependencies) { + super(dependencies, ModuleDependenciesAssertion.class); + } + + void containOnlyModuleDependencies(ExpectedModuleDependency... expectedModuleDependencies) { + assertThat(actual).as("actual module dependencies").hasSameSizeAs(expectedModuleDependencies); + + List unmatchedDependencies = newArrayList(expectedModuleDependencies); + unmatchedDependencies.removeIf(expectedModuleDependency -> actual.stream().anyMatch(expectedModuleDependency::matches)); + assertThat(unmatchedDependencies).as("unmatched module dependencies").isEmpty(); + } + + static class ExpectedModuleDependency { + private final Identifier origin; + private final Identifier target; + private final Optional expectedDependencies; + + private ExpectedModuleDependency(Identifier origin, Identifier target) { + this(origin, target, Optional.empty()); + } + + private ExpectedModuleDependency(Identifier origin, Identifier target, + Optional expectedDependencies) { + this.origin = origin; + this.target = target; + this.expectedDependencies = expectedDependencies; + } + + static Creator from(String... identifier) { + return new Creator(Identifier.from(identifier)); + } + + boolean matches(ModuleDependency moduleDependency) { + if (!moduleDependency.getOrigin().getIdentifier().equals(origin) || !moduleDependency.getTarget().getIdentifier().equals(target)) { + return false; + } + if (!expectedDependencies.isPresent()) { + return true; + } + + Set actualClassDependencies = moduleDependency.toClassDependencies(); + ExpectedDependencies.MatchResult result = expectedDependencies.get().match(actualClassDependencies); + return result.matchesExactly(); + } + + @Override + public String toString() { + return String.format("Expected Module Dependency [%s -> %s] {%s}", origin, target, expectedDependencies); + } + + public ExpectedModuleDependency withClassDependencies(ExpectedDependencies expectedDependencies) { + return new ExpectedModuleDependency(origin, target, Optional.of(expectedDependencies)); + } + + static class Creator { + private final Identifier origin; + + Creator(Identifier origin) { + this.origin = origin; + } + + ExpectedModuleDependency to(String... identifier) { + return new ExpectedModuleDependency(origin, Identifier.from(identifier)); + } + } + } + } + + private static class TestModuleDescriptor implements ArchModule.Descriptor { + private final String name; + private final JavaClass descriptorClass; + + TestModuleDescriptor(String name, JavaClass descriptorClass) { + this.name = name; + this.descriptorClass = descriptorClass; + } + + @Override + public String getName() { + return name; + } + + JavaClass getDescriptorClass() { + return descriptorClass; + } + } + + private @interface AnnotationWithIncompatibleNameProperty { + int name(); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/RandomModulesSyntaxTest.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/RandomModulesSyntaxTest.java new file mode 100644 index 0000000000..4a6265e495 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/RandomModulesSyntaxTest.java @@ -0,0 +1,144 @@ +package com.tngtech.archunit.library.modules; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.google.common.reflect.TypeToken; +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.library.modules.syntax.AllowedModuleDependencies; +import com.tngtech.archunit.library.modules.syntax.DescriptorFunction; +import com.tngtech.archunit.library.modules.syntax.GivenModules; +import com.tngtech.archunit.library.modules.syntax.GivenModulesByAnnotation; +import com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope; +import com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition; +import com.tngtech.archunit.testutil.syntax.Parameter; +import com.tngtech.archunit.testutil.syntax.RandomSyntaxSeed; +import com.tngtech.archunit.testutil.syntax.RandomSyntaxTestBase; +import com.tngtech.archunit.testutil.syntax.SingleParameterProvider; +import com.tngtech.java.junit.dataprovider.DataProvider; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.testutil.syntax.MethodChoiceStrategy.chooseAllArchUnitSyntaxMethods; +import static java.util.stream.Collectors.toList; + +public class RandomModulesSyntaxTest extends RandomSyntaxTestBase { + @DataProvider + public static List> random_rules() { + return createRandomRulesForSeeds( + new RandomSyntaxSeed<>( + givenModulesClass(), + ModuleRuleDefinition.modules().definedByPackages("..test.(*).."), + "modules defined by packages '..test.(*)..'"), + new RandomSyntaxSeed<>( + givenModulesByAnnotationClass(), + ModuleRuleDefinition.modules().definedByAnnotation(RandomSyntaxModule.class), + "modules defined by annotation @" + RandomSyntaxModule.class.getSimpleName()), + new RandomSyntaxSeed<>( + givenModulesByAnnotationClass(), + ModuleRuleDefinition.modules().definedByAnnotation(RandomSyntaxModule.class, RandomSyntaxModule::name), + "modules defined by annotation @" + RandomSyntaxModule.class.getSimpleName()), + new RandomSyntaxSeed<>( + givenModulesClass(), + ModuleRuleDefinition.modules() + .definedByRootClasses(DescribedPredicate.describe("some predicate", alwaysTrue())) + .derivingModuleFromRootClassBy(DescribedFunction.describe("some function", it -> ArchModule.Descriptor.create("irrelevant"))), + "modules defined by root classes some predicate deriving module from root class by some function"), + new RandomSyntaxSeed<>( + givenModulesClass(), + ModuleRuleDefinition.modules() + .definedBy(DescribedFunction.describe("some function", it -> ArchModule.Identifier.ignore())) + .derivingModule(DescriptorFunction.describe("some other function", (__, ___) -> ArchModule.Descriptor.create("irrelevant"))), + "modules defined by some function deriving module some other function") + ); + } + + @SafeVarargs + private static List> createRandomRulesForSeeds(RandomSyntaxSeed>... seeds) { + return Arrays.stream(seeds) + .map(seed -> RandomSyntaxTestBase.createRandomRules( + RandomRulesBlueprint + .seed(seed) + .methodChoiceStrategy(chooseAllArchUnitSyntaxMethods().exceptMethodsWithName("ignoreDependency")) + .parameterProviders( + new SingleParameterProvider(ModuleDependencyScope.class) { + @Override + public Parameter get(String methodName, TypeToken type) { + ModuleDependencyScope dependencyScope = randomElement( + ModuleDependencyScope.consideringAllDependencies(), + ModuleDependencyScope.consideringOnlyDependenciesBetweenModules(), + ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage("..test..") + ); + return new Parameter(dependencyScope, dependencyScope.getDescription()); + } + + @SafeVarargs + private final T randomElement(T... elements) { + return elements[random.nextInt(elements.length)]; + } + }, + new SingleParameterProvider(AllowedModuleDependencies.class) { + @Override + public Parameter get(String methodName, TypeToken type) { + return new Parameter( + AllowedModuleDependencies.allow() + .fromModule("Module One").toModules("Module Two", "Module Three") + .fromModule("Module Two").toModules("Module Three"), + "{ Module One -> [Module Two, Module Three], Module Two -> [Module Three] }" + ); + } + }, + new SingleParameterProvider(String.class) { + @Override + protected boolean canHandle(String methodName, Class type) { + return methodName.equals("respectTheirAllowedDependenciesDeclaredIn") && super.canHandle(methodName, type); + } + + @Override + public Parameter get(String methodName, TypeToken type) { + return new Parameter("allowedDependencies", "'allowedDependencies'"); + } + }, + new SingleParameterProvider(String.class) { + + @Override + protected boolean canHandle(String methodName, Class type) { + return methodName.equals("onlyDependOnEachOtherThroughPackagesDeclaredIn") && super.canHandle(methodName, type); + } + + @Override + public Parameter get(String methodName, TypeToken type) { + return new Parameter("exposedPackages", "'exposedPackages'"); + } + } + ) + .descriptionReplacements( + new ReplaceEverythingSoFar("as '([^']+)'.*", "$1"), + new SingleStringReplacement("meta annotated", "meta-annotated") + ) + ) + ) + .flatMap(Collection::stream) + .collect(toList()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Class> givenModulesClass() { + return (Class) GivenModules.class; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Class> givenModulesByAnnotationClass() { + return (Class) GivenModulesByAnnotation.class; + } + + @SuppressWarnings("unused") + private @interface RandomSyntaxModule { + String name(); + + String[] allowedDependencies() default {}; + + String[] exposedPackages() default {}; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/GivenModulesTest.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/GivenModulesTest.java new file mode 100644 index 0000000000..c85e08e4de --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/GivenModulesTest.java @@ -0,0 +1,83 @@ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.function.Predicate; + +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.testutil.ArchConfigurationRule; +import org.junit.Rule; +import org.junit.Test; + +import static com.tngtech.archunit.core.domain.TestUtils.importClasses; +import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; +import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; +import static com.tngtech.archunit.testutil.Assertions.assertThatRule; + +public class GivenModulesTest { + + @Rule + public final ArchConfigurationRule archConfigurationRule = new ArchConfigurationRule(); + + // note that description logic is already covered by RandomModulesSyntaxTest + + @Test + public void allows_restricting_modules() { + archConfigurationRule.setFailOnEmptyShould(false); + + assertThatRule(modulesByClassName().should(alwaysBeViolations())) + .checking(importClasses(Object.class, String.class)) + .hasNumberOfViolations(2); + + assertThatRule( + modulesByClassName() + .that(getPredicate(m -> m.getName().contains("Object"))) + .should(alwaysBeViolations())) + .checking(importClasses(Object.class, String.class)) + .hasNumberOfViolations(1); + + assertThatRule( + modulesByClassName() + .that(getPredicate(m -> m.getName().contains("Object"))) + .or(getPredicate(m -> m.getName().contains("String"))) + .should(alwaysBeViolations())) + .checking(importClasses(Object.class, String.class)) + .hasNumberOfViolations(2); + + assertThatRule( + modulesByClassName() + .that(getPredicate(m -> m.getName().contains("Object"))) + .and(getPredicate(m -> m.getName().contains("String"))) + .should(alwaysBeViolations())) + .checking(importClasses(Object.class, String.class)) + .hasNoViolation(); + } + + private static DescribedPredicate> getPredicate(Predicate> predicate) { + return DescribedPredicate.describe("", predicate); + } + + static GivenModules modulesByClassName() { + return modules().definedBy(getClassName()).derivingModule(fromClassName()); + } + + private static DescriptorFunction fromClassName() { + return DescriptorFunction.describe("from class name", (identifier, __) -> ArchModule.Descriptor.create(identifier.getPart(1))); + } + + private static DescribedFunction getClassName() { + return DescribedFunction.describe("class name", clazz -> ArchModule.Identifier.from(clazz.getName())); + } + + private static ArchCondition> alwaysBeViolations() { + return new ArchCondition>("always be violations") { + @Override + public void check(ArchModule module, ConditionEvents events) { + events.add(violated(module, "violation of " + module.getName())); + } + }; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/ModuleRuleTest.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/ModuleRuleTest.java new file mode 100644 index 0000000000..40d7527185 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/ModuleRuleTest.java @@ -0,0 +1,132 @@ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.function.Function; + +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.TestAnnotation; +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.TestAnnotationCustomName; +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.one.ClassOne; +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.two.ClassTwo; +import org.junit.Test; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysFalse; +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesBetweenModules; +import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; +import static com.tngtech.archunit.testutil.Assertions.assertThatRule; + +public class ModuleRuleTest { + + @Test + public void definedByPackages_default_name() { + assertThatRule( + modules() + .definedByPackages("..test_modules.(*).(*)..") + .should(reportAllAsViolations(ArchModule::getName)) + ) + .checking(new ClassFileImporter().importPackagesOf(ClassOne.class, ClassTwo.class)) + .hasOnlyViolations("one:one", "one:two", "two:one"); + } + + @Test + public void definedByPackages_custom_name() { + assertThatRule( + modules() + .definedByPackages("..test_modules.(*).(*)..") + .derivingNameFromPattern("Test-Module($1-$2)") + .should(reportAllAsViolations(ArchModule::getName)) + ) + .checking(new ClassFileImporter().importPackagesOf(ClassOne.class, ClassTwo.class)) + .hasOnlyViolations("Test-Module(one-one)", "Test-Module(one-two)", "Test-Module(two-one)"); + } + + @Test + public void definedByRootClasses_default_name() { + assertThatRule( + modules() + .definedByRootClasses(equivalentTo(ClassOne.class).or(equivalentTo(ClassTwo.class))) + .should(reportAllAsViolations(ArchModule::getName)) + ) + .checking(new ClassFileImporter().importPackagesOf(ClassOne.class, ClassTwo.class)) + .hasOnlyViolations(ClassOne.class.getPackage().getName(), ClassTwo.class.getPackage().getName()); + } + + @Test + public void definedByRootClasses_custom_name() { + assertThatRule( + modules() + .definedByRootClasses(equivalentTo(ClassOne.class).or(equivalentTo(ClassTwo.class))) + .derivingModuleFromRootClassBy(DescribedFunction.describe("simple class name", javaClass -> ArchModule.Descriptor.create(javaClass.getSimpleName()))) + .should(reportAllAsViolations(ArchModule::getName)) + ) + .checking(new ClassFileImporter().importPackagesOf(ClassOne.class, ClassTwo.class)) + .hasOnlyViolations(ClassOne.class.getSimpleName(), ClassTwo.class.getSimpleName()); + } + + @Test + public void definedByAnnotation_default_name() { + assertThatRule( + modules() + .definedByAnnotation(TestAnnotation.class) + .should(reportAllAsViolations(ArchModule::getName)) + ) + .checking(new ClassFileImporter().importPackagesOf(ClassOne.class, ClassTwo.class)) + .hasOnlyViolations("one", "two"); + } + + @Test + public void definedByAnnotation_custom_name() { + assertThatRule( + modules() + .definedByAnnotation(TestAnnotationCustomName.class, TestAnnotationCustomName::customName) + .should(reportAllAsViolations(ArchModule::getName)) + ) + .checking(new ClassFileImporter().importPackagesOf(ClassOne.class, ClassTwo.class)) + .hasOnlyViolations("customOne", "customTwo"); + } + + @Test + public void definedBy_allows_generic_customization() { + assertThatRule( + modules() + .definedBy(DescribedFunction.describe("simple class name", + javaClass -> ArchModule.Identifier.from(javaClass.getSimpleName()))) + .derivingModule(DescriptorFunction.describe("from identifier", + (identifier, __) -> ArchModule.Descriptor.create(identifier.getPart(1)))) + .should(reportAllAsViolations(ArchModule::getName)) + ) + .checking(new ClassFileImporter().importClasses(ClassOne.class, ClassTwo.class)) + .hasOnlyViolations(ClassOne.class.getSimpleName(), ClassTwo.class.getSimpleName()); + } + + @Test + public void ignoring_dependencies_can_be_applied_before_other_methods() { + assertThatRule( + modules() + .definedByAnnotation(TestAnnotation.class) + .should().respectTheirAllowedDependencies(alwaysFalse(), consideringOnlyDependenciesBetweenModules()) + .ignoreDependency(equivalentTo(ClassOne.class), alwaysTrue()) + .because("reason") + .as("description") + .allowEmptyShould(false) + ) + .checking(new ClassFileImporter().importPackagesOf(ClassOne.class, ClassTwo.class)) + .hasNoViolation(); + } + + private static ArchCondition> reportAllAsViolations(Function, String> reportModule) { + return new ArchCondition>("report all as violations") { + @Override + public void check(ArchModule module, ConditionEvents events) { + events.add(SimpleConditionEvent.violated(module, reportModule.apply(module))); + } + }; + } + +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldTest.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldTest.java new file mode 100644 index 0000000000..93e6ce1887 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/ModulesShouldTest.java @@ -0,0 +1,302 @@ +package com.tngtech.archunit.library.modules.syntax; + +import java.util.function.Function; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.testexamples.default_annotation.TestModule; +import com.tngtech.archunit.library.modules.testexamples.default_annotation.module1.ClassInModule1; +import com.tngtech.archunit.library.modules.testexamples.default_annotation.module2.InternalClassInModule2; +import com.tngtech.archunit.library.modules.testexamples.default_annotation.module2.api.ApiClassInModule2; +import com.tngtech.archunit.library.modules.testexamples.default_annotation.module3.ClassInModule3; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysFalse; +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleName; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; +import static com.tngtech.archunit.library.modules.syntax.GivenModulesTest.modulesByClassName; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringAllDependencies; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesBetweenModules; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; +import static com.tngtech.archunit.testutil.Assertions.assertThatRule; +import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; +import static java.util.regex.Pattern.quote; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(DataProviderRunner.class) +public class ModulesShouldTest { + + @Test + public void respectTheirAllowedDependencies_considering_all_dependencies() { + assertThatRule(modulesByClassName().should().respectTheirAllowedDependencies(alwaysFalse(), consideringAllDependencies())) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasViolationContaining(String.class.getName()) + .hasViolationContaining(ArchRule.class.getName()); + } + + @Test + public void respectTheirAllowedDependencies_considering_only_dependencies_between_modules() { + assertThatRule(modulesByClassName().should().respectTheirAllowedDependencies(alwaysFalse(), consideringOnlyDependenciesBetweenModules())) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasNoViolationContaining(String.class.getName()) + .hasNoViolationContaining(ArchRule.class.getName()); + } + + @Test + public void respectTheirAllowedDependencies_considering_only_dependencies_in_packages() { + assertThatRule(modulesByClassName().should().respectTheirAllowedDependencies( + alwaysFalse(), + consideringOnlyDependenciesInAnyPackage(getClass().getPackage().getName() + "..", ArchRule.class.getPackage().getName() + "..") + )) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasNoViolationContaining(String.class.getName()) + .hasViolationContaining(ArchRule.class.getName()); + } + + @Test + public void respectTheirAllowedDependenciesDeclaredIn_takes_allowed_dependencies_from_annotation_property() { + assertThatRule(modules().definedByAnnotation(TestModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", consideringOnlyDependenciesBetweenModules())) + .checking(new ClassFileImporter().importPackagesOf(ClassInModule1.class, InternalClassInModule2.class, ClassInModule3.class)) + .hasViolationContaining(ClassInModule3.class.getName()) + .hasNoViolationContaining(ApiClassInModule2.class.getName()) + .hasNoViolationContaining(InternalClassInModule2.class.getName()); + } + + @Test + public void respectTheirAllowedDependenciesDeclaredIn_works_together_with_filtering_by_predicate() { + assertThatRule(modules().definedByAnnotation(TestModule.class) + .that(DescribedPredicate.describe("are not Module 1", it -> !it.getName().equals("Module 1"))) + .and(DescribedPredicate.describe("are not Module 2", it -> !it.getName().equals("Module 2"))) + .or(DescribedPredicate.describe("are Module 3", it -> it.getName().equals("Module 3"))) + .should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", consideringOnlyDependenciesBetweenModules())) + .checking(new ClassFileImporter().importPackagesOf(ClassInModule1.class, InternalClassInModule2.class, ClassInModule3.class)) + .hasNoViolation(); + } + + @Test + public void respectTheirAllowedDependenciesDeclaredIn_rejects_missing_property() { + assertThatThrownBy( + () -> modules().definedByAnnotation(TestModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("notThere", consideringOnlyDependenciesBetweenModules()) + .evaluate(new ClassFileImporter().importPackagesOf(ClassInModule1.class, InternalClassInModule2.class)) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format("Could not invoke @%s.notThere()", TestModule.class.getSimpleName())); + } + + @Test + public void respectTheirAllowedDependenciesDeclaredIn_rejects_property_of_wrong_type() { + assertThatThrownBy( + () -> modules().definedByAnnotation(TestModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("name", consideringOnlyDependenciesBetweenModules()) + .evaluate(new ClassFileImporter().importPackagesOf(ClassInModule1.class, InternalClassInModule2.class)) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format("Property @%s.name() must be of type %s", TestModule.class.getSimpleName(), String[].class.getSimpleName())); + } + + @Test + public void onlyDependOnEachOtherThroughPackagesDeclaredIn_takes_allowed_dependencies_from_annotation_property() { + assertThatRule(modules().definedByAnnotation(TestModule.class) + .should().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages")) + .checking(new ClassFileImporter().importPackagesOf(ClassInModule1.class, InternalClassInModule2.class)) + .hasViolationContaining(InternalClassInModule2.class.getName()) + .hasNoViolationContaining(ApiClassInModule2.class.getName()); + } + + @Test + public void onlyDependOnEachOtherThroughPackagesDeclaredIn_rejects_missing_property() { + assertThatThrownBy( + () -> modules().definedByAnnotation(TestModule.class) + .should().onlyDependOnEachOtherThroughPackagesDeclaredIn("notThere") + .evaluate(new ClassFileImporter().importPackagesOf(ClassInModule1.class, InternalClassInModule2.class)) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format("Could not invoke @%s.notThere()", TestModule.class.getSimpleName())); + } + + @Test + public void onlyDependOnEachOtherThroughPackagesDeclaredIn_rejects_property_of_wrong_type() { + assertThatThrownBy( + () -> modules().definedByAnnotation(TestModule.class) + .should().onlyDependOnEachOtherThroughPackagesDeclaredIn("name") + .evaluate(new ClassFileImporter().importPackagesOf(ClassInModule1.class, InternalClassInModule2.class)) + ) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.format("Property @%s.name() must be of type %s", TestModule.class.getSimpleName(), String[].class.getSimpleName())); + } + + @DataProvider + public static Object[][] ruleModificationsToIgnoreViolationsFromModuleTwoToArchRule() { + return testForEach( + (Function, ModulesRule>) modulesRule -> modulesRule.ignoreDependency(ModuleTwo.class, ArchRule.class), + (Function, ModulesRule>) modulesRule -> modulesRule.ignoreDependency(ModuleTwo.class.getName(), ArchRule.class.getName()), + (Function, ModulesRule>) modulesRule -> modulesRule.ignoreDependency(equivalentTo(ModuleTwo.class), equivalentTo(ArchRule.class)), + (Function, ModulesRule>) modulesRule -> modulesRule.ignoreDependency(dependency(ModuleTwo.class, ArchRule.class)) + ); + } + + @Test + @UseDataProvider("ruleModificationsToIgnoreViolationsFromModuleTwoToArchRule") + public void respectTheirAllowedDependencies_ignores_dependencies(Function, ModulesRule> modifyRuleToIgnoreViolationsFromModuleTwoToArchRule) { + ModulesRule rule = modulesByClassName().should().respectTheirAllowedDependencies(alwaysFalse(), consideringAllDependencies()); + + rule = modifyRuleToIgnoreViolationsFromModuleTwoToArchRule.apply(rule); + + assertThatRule(rule) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasViolationContaining(String.class.getName()) + .hasNoViolationContaining(ArchRule.class.getName()); + } + + @Test + public void respectTheirAllowedDependencies_filtered_by_that_ignores_dependencies() { + assertThatRule( + modulesByClassName() + .that(describe("are not Module Two", m -> !m.getName().endsWith(ModuleTwo.class.getSimpleName()))) + .should().respectTheirAllowedDependencies(alwaysFalse(), consideringAllDependencies()) + ) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasViolationContaining(String.class.getName()) + .hasNoViolationContaining(ArchRule.class.getName()); + } + + @DataProvider + public static Object[][] data_onlyDependOnEachOtherThroughClassesThat_ignores_dependencies() { + return testForEach( + modulesByClassName().should().onlyDependOnEachOtherThroughClassesThat(have(simpleName(ModuleTwo.class.getSimpleName()))), + modulesByClassName().should().onlyDependOnEachOtherThroughClassesThat().haveSimpleName(ModuleTwo.class.getSimpleName()) + ); + } + + @Test + @UseDataProvider + public void test_onlyDependOnEachOtherThroughClassesThat_ignores_dependencies(ModulesRule rule) { + rule = rule + .ignoreDependency(d -> d.getDescription().contains("cyclicDependencyOne")); + + assertThatRule(rule) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasOnlyOneViolationContaining("cyclicDependencyTwo") + .hasNoViolationContaining("cyclicDependencyOne"); + + rule = rule.ignoreDependency(d -> d.getDescription().contains("cyclicDependencyTwo")); + + assertThatRule(rule) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasNoViolation(); + } + + @Test + public void beFreeOfCycles_ignores_dependencies() { + ModulesRule rule = modulesByClassName().should().beFreeOfCycles().ignoreDependency(d -> d.getDescription().contains("cyclicDependencyOne")); + + assertThatRule(rule) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasOnlyOneViolationContaining("Cycle detected") + .hasViolationContaining("cyclicDependencyTwo") + .hasNoViolationContaining("cyclicDependencyOne"); + + rule = rule.ignoreDependency(d -> d.getDescription().contains("cyclicDependencyTwo")); + + assertThatRule(rule) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasNoViolation(); + } + + @Test + public void andShould_joins_predefined_conditions() { + assertThatRule( + modulesByClassName() + .should().onlyDependOnEachOtherThroughClassesThat(have(simpleName(ModuleTwo.class.getSimpleName()))) + .andShould().beFreeOfCycles() + ) + .hasDescriptionContaining("only depend on each other") + .hasDescriptionContaining("be free of cycles") + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + // from checking for cycles we should get a violation starting with a '-' + .hasViolationContaining("- Field <%s.cyclicDependencyOne>", ModuleTwo.class.getName()) + // from checking how modules depend on each other we should get the same violation, but not starting with a '-' + .hasViolationMatching("^" + quote("Field <" + ModuleTwo.class.getName() + ".cyclicDependencyOne>") + ".*"); + } + + @Test + public void andShould_joins_custom_condition() { + assertThatRule( + modulesByClassName() + .should().beFreeOfCycles() + .andShould(new ArchCondition>("not contain 'cyclicDependencyOne'") { + @Override + public void check(ArchModule module, ConditionEvents events) { + module.getClassDependenciesFromSelf() + .stream().filter(it -> it.getDescription().contains("cyclicDependencyOne")) + .forEach(it -> events.add(SimpleConditionEvent.violated(it, "custom: cyclicDependencyOne"))); + } + }) + ) + .hasDescriptionContaining("be free of cycles") + .hasDescriptionContaining("not contain 'cyclicDependencyOne'") + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + // from checking for cycles we should get a violation starting with a '-' + .hasViolationContaining("- Field <%s.cyclicDependencyOne>", ModuleTwo.class.getName()) + .hasViolation("custom: cyclicDependencyOne"); + } + + @Test + public void andShould_only_ignores_dependencies_of_last_condition() { + assertThatRule( + modulesByClassName() + .should().onlyDependOnEachOtherThroughClassesThat(have(simpleName(ModuleTwo.class.getSimpleName()))) + .ignoreDependency(d -> d.getDescription().contains("cyclicDependencyOne")) + .andShould().beFreeOfCycles() + ) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasViolationContaining("cyclicDependencyOne"); + + assertThatRule( + modulesByClassName() + .should().onlyDependOnEachOtherThroughClassesThat(have(simpleName(ModuleTwo.class.getSimpleName()))) + .andShould().beFreeOfCycles() + .ignoreDependency(d -> d.getDescription().contains("cyclicDependencyOne")) + ) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasViolationContaining("cyclicDependencyOne"); + + assertThatRule( + modulesByClassName() + .should().onlyDependOnEachOtherThroughClassesThat(have(simpleName(ModuleTwo.class.getSimpleName()))) + .ignoreDependency(d -> d.getDescription().contains("cyclicDependencyOne")) + .andShould().beFreeOfCycles() + .ignoreDependency(d -> d.getDescription().contains("cyclicDependencyOne")) + ) + .checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class)) + .hasNoViolationContaining("cyclicDependencyOne"); + } + + @SuppressWarnings("unused") + private static class ModuleOne { + ModuleTwo dependencyToOtherModule; + String dependencyToStandardJavaClass; + } + + @SuppressWarnings("unused") + private static class ModuleTwo { + ModuleOne cyclicDependencyOne; + ModuleOne cyclicDependencyTwo; + ArchRule dependencyInOtherArchUnitPackage; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/TestAnnotation.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/TestAnnotation.java new file mode 100644 index 0000000000..a6cd17c7b9 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/TestAnnotation.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.syntax.testexamples.test_modules; + +public @interface TestAnnotation { + String name(); +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/TestAnnotationCustomName.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/TestAnnotationCustomName.java new file mode 100644 index 0000000000..c9412ae5cb --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/TestAnnotationCustomName.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.syntax.testexamples.test_modules; + +public @interface TestAnnotationCustomName { + String customName(); +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/ClassOne.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/ClassOne.java new file mode 100644 index 0000000000..5633b6f40c --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/ClassOne.java @@ -0,0 +1,12 @@ +package com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.one; + +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.TestAnnotation; +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.TestAnnotationCustomName; +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.two.ClassTwo; + +@SuppressWarnings("unused") +@TestAnnotation(name = "one") +@TestAnnotationCustomName(customName = "customOne") +public class ClassOne { + ClassTwo classTwo; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/one/ClassOneOne.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/one/ClassOneOne.java new file mode 100644 index 0000000000..beeded1ae2 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/one/ClassOneOne.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.one.one; + +@SuppressWarnings("unused") +public class ClassOneOne { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/two/ClassTwoTwo.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/two/ClassTwoTwo.java new file mode 100644 index 0000000000..94ed51c201 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/one/two/ClassTwoTwo.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.one.two; + +@SuppressWarnings("unused") +public class ClassTwoTwo { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/two/ClassTwo.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/two/ClassTwo.java new file mode 100644 index 0000000000..eae3585857 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/two/ClassTwo.java @@ -0,0 +1,9 @@ +package com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.two; + +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.TestAnnotation; +import com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.TestAnnotationCustomName; + +@TestAnnotation(name = "two") +@TestAnnotationCustomName(customName = "customTwo") +public class ClassTwo { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/two/one/ClassTwoOne.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/two/one/ClassTwoOne.java new file mode 100644 index 0000000000..d1f9a4a57c --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/syntax/testexamples/test_modules/two/one/ClassTwoOne.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.syntax.testexamples.test_modules.two.one; + +@SuppressWarnings("unused") +public class ClassTwoOne { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/MyModule.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/MyModule.java new file mode 100644 index 0000000000..6f92da1be1 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/MyModule.java @@ -0,0 +1,10 @@ +package com.tngtech.archunit.library.modules.testexamples; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +public @interface MyModule { + String name(); +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/annotation_with_custom_name/MyModuleWithCustomName.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/annotation_with_custom_name/MyModuleWithCustomName.java new file mode 100644 index 0000000000..0c1afa2b1e --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/annotation_with_custom_name/MyModuleWithCustomName.java @@ -0,0 +1,10 @@ +package com.tngtech.archunit.library.modules.testexamples.annotation_with_custom_name; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +public @interface MyModuleWithCustomName { + String customName(); +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/annotation_with_custom_name/module1/ModuleOneDescriptorCustomName.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/annotation_with_custom_name/module1/ModuleOneDescriptorCustomName.java new file mode 100644 index 0000000000..41d0348a21 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/annotation_with_custom_name/module1/ModuleOneDescriptorCustomName.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.library.modules.testexamples.annotation_with_custom_name.module1; + +import com.tngtech.archunit.library.modules.testexamples.annotation_with_custom_name.MyModuleWithCustomName; + +@MyModuleWithCustomName(customName = "Module One") +public class ModuleOneDescriptorCustomName { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/TestModule.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/TestModule.java new file mode 100644 index 0000000000..f229ddba3c --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/TestModule.java @@ -0,0 +1,9 @@ +package com.tngtech.archunit.library.modules.testexamples.default_annotation; + +public @interface TestModule { + String name(); + + String[] allowedDependencies() default {}; + + String[] exposedPackages() default {}; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module1/ClassInModule1.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module1/ClassInModule1.java new file mode 100644 index 0000000000..3ee5eb71bd --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module1/ClassInModule1.java @@ -0,0 +1,12 @@ +package com.tngtech.archunit.library.modules.testexamples.default_annotation.module1; + +import com.tngtech.archunit.library.modules.testexamples.default_annotation.module2.InternalClassInModule2; +import com.tngtech.archunit.library.modules.testexamples.default_annotation.module2.api.ApiClassInModule2; +import com.tngtech.archunit.library.modules.testexamples.default_annotation.module3.ClassInModule3; + +@SuppressWarnings("unused") +public class ClassInModule1 { + ApiClassInModule2 allowedDependency; + ClassInModule3 forbiddenDependencyBecauseWrongModule; + InternalClassInModule2 forbiddenDependencyBecauseWrongPackage; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module1/package-info.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module1/package-info.java new file mode 100644 index 0000000000..76b0c6f4eb --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module1/package-info.java @@ -0,0 +1,4 @@ +@TestModule(name = "Module 1", allowedDependencies = "Module 2") +package com.tngtech.archunit.library.modules.testexamples.default_annotation.module1; + +import com.tngtech.archunit.library.modules.testexamples.default_annotation.TestModule; diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/InternalClassInModule2.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/InternalClassInModule2.java new file mode 100644 index 0000000000..07b6457beb --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/InternalClassInModule2.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.default_annotation.module2; + +public class InternalClassInModule2 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/api/ApiClassInModule2.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/api/ApiClassInModule2.java new file mode 100644 index 0000000000..6e72cd0925 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/api/ApiClassInModule2.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.default_annotation.module2.api; + +public class ApiClassInModule2 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/package-info.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/package-info.java new file mode 100644 index 0000000000..bfacd1935d --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module2/package-info.java @@ -0,0 +1,4 @@ +@TestModule(name = "Module 2", exposedPackages = "..api..") +package com.tngtech.archunit.library.modules.testexamples.default_annotation.module2; + +import com.tngtech.archunit.library.modules.testexamples.default_annotation.TestModule; diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module3/ClassInModule3.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module3/ClassInModule3.java new file mode 100644 index 0000000000..3c5bb8c80f --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module3/ClassInModule3.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.default_annotation.module3; + +public class ClassInModule3 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module3/package-info.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module3/package-info.java new file mode 100644 index 0000000000..60fa64f609 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/default_annotation/module3/package-info.java @@ -0,0 +1,4 @@ +@TestModule(name = "Module 3") +package com.tngtech.archunit.library.modules.testexamples.default_annotation.module3; + +import com.tngtech.archunit.library.modules.testexamples.default_annotation.TestModule; diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/invalid/overlapping_root_classes/ModuleOneDescriptor.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/invalid/overlapping_root_classes/ModuleOneDescriptor.java new file mode 100644 index 0000000000..9e4cc04c92 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/invalid/overlapping_root_classes/ModuleOneDescriptor.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.library.modules.testexamples.invalid.overlapping_root_classes; + +import com.tngtech.archunit.library.modules.testexamples.MyModule; + +@MyModule(name = "Module One") +public class ModuleOneDescriptor { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/invalid/overlapping_root_classes/child/ModuleTwoDescriptor.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/invalid/overlapping_root_classes/child/ModuleTwoDescriptor.java new file mode 100644 index 0000000000..c965eb7017 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/invalid/overlapping_root_classes/child/ModuleTwoDescriptor.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.library.modules.testexamples.invalid.overlapping_root_classes.child; + +import com.tngtech.archunit.library.modules.testexamples.MyModule; + +@MyModule(name = "Module Two") +public class ModuleTwoDescriptor { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/FirstClassInModule1.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/FirstClassInModule1.java new file mode 100644 index 0000000000..5833987ebf --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/FirstClassInModule1.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module1; + +public class FirstClassInModule1 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/ModuleOneDescriptor.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/ModuleOneDescriptor.java new file mode 100644 index 0000000000..597933dc57 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/ModuleOneDescriptor.java @@ -0,0 +1,8 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module1; + +import com.tngtech.archunit.library.modules.testexamples.MyModule; + +@MyModule(name = "Module One") +public interface ModuleOneDescriptor { + String name = "Module One"; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/SecondClassInModule1.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/SecondClassInModule1.java new file mode 100644 index 0000000000..bab0702989 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/SecondClassInModule1.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module1; + +public class SecondClassInModule1 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub1/FirstClassInSubModule11.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub1/FirstClassInSubModule11.java new file mode 100644 index 0000000000..fa7bb001ed --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub1/FirstClassInSubModule11.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module1.sub1; + +import java.util.List; + +import com.tngtech.archunit.library.modules.testexamples.valid.module1.sub2.FirstClassInSubModule12; +import com.tngtech.archunit.library.modules.testexamples.valid.module1.sub2.SecondClassInSubModule12; + +@SuppressWarnings("unused") +public class FirstClassInSubModule11 { + SecondClassInSubModule11 noDependencyToOtherModule; + SecondClassInSubModule11[] noDependencyToOtherModuleByArrayType; + FirstClassInSubModule12[] firstDependencyOnSubModule12; + SecondClassInSubModule12[][] secondDependencyOnSubModule12; + + String firstUndefinedDependency; + List secondUndefinedDependency; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub1/SecondClassInSubModule11.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub1/SecondClassInSubModule11.java new file mode 100644 index 0000000000..fe49c55bbe --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub1/SecondClassInSubModule11.java @@ -0,0 +1,14 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module1.sub1; + +import java.util.Collection; + +import com.tngtech.archunit.library.modules.testexamples.valid.module2.FirstClassInModule2; +import com.tngtech.archunit.library.modules.testexamples.valid.module2.sub1.FirstClassInSubModule21; + +@SuppressWarnings("unused") +public class SecondClassInSubModule11 { + FirstClassInModule2 dependencyOnModule2; + FirstClassInSubModule21 dependencyOnSubModule21; + + Collection thirdUndefinedDependency; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub2/FirstClassInSubModule12.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub2/FirstClassInSubModule12.java new file mode 100644 index 0000000000..547bad8977 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub2/FirstClassInSubModule12.java @@ -0,0 +1,8 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module1.sub2; + +import com.tngtech.archunit.library.modules.testexamples.valid.module2.sub1.FirstClassInSubModule21; + +@SuppressWarnings("unused") +public class FirstClassInSubModule12 { + FirstClassInSubModule21[][] dependencyOnSubModule21; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub2/SecondClassInSubModule12.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub2/SecondClassInSubModule12.java new file mode 100644 index 0000000000..321eea8b73 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module1/sub2/SecondClassInSubModule12.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module1.sub2; + +public class SecondClassInSubModule12 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/FirstClassInModule2.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/FirstClassInModule2.java new file mode 100644 index 0000000000..5695f73c56 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/FirstClassInModule2.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module2; + +public class FirstClassInModule2 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/ModuleTwoDescriptor.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/ModuleTwoDescriptor.java new file mode 100644 index 0000000000..b32b6348fb --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/ModuleTwoDescriptor.java @@ -0,0 +1,9 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module2; + +import com.tngtech.archunit.library.modules.testexamples.MyModule; + +@SuppressWarnings("unused") +@MyModule(name = "Module Two") +public interface ModuleTwoDescriptor { + String name = "Module Two"; +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/SecondClassInModule2.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/SecondClassInModule2.java new file mode 100644 index 0000000000..d414509d8e --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/SecondClassInModule2.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module2; + +@SuppressWarnings("unused") +public class SecondClassInModule2 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub1/FirstClassInSubModule21.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub1/FirstClassInSubModule21.java new file mode 100644 index 0000000000..d448ff0c65 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub1/FirstClassInSubModule21.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module2.sub1; + +public class FirstClassInSubModule21 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub1/SecondClassInSubModule21.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub1/SecondClassInSubModule21.java new file mode 100644 index 0000000000..09a25209f1 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub1/SecondClassInSubModule21.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module2.sub1; + +@SuppressWarnings("unused") +public class SecondClassInSubModule21 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub2/FirstClassInSubModule22.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub2/FirstClassInSubModule22.java new file mode 100644 index 0000000000..e3aa8d98db --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub2/FirstClassInSubModule22.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module2.sub2; + +@SuppressWarnings("unused") +public class FirstClassInSubModule22 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub2/SecondClassInSubModule22.java b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub2/SecondClassInSubModule22.java new file mode 100644 index 0000000000..eea228a53b --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/modules/testexamples/valid/module2/sub2/SecondClassInSubModule22.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.library.modules.testexamples.valid.module2.sub2; + +@SuppressWarnings("unused") +public class SecondClassInSubModule22 { +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/ArchConfigurationRule.java b/archunit/src/test/java/com/tngtech/archunit/testutil/ArchConfigurationRule.java index 4795c84c43..a1a0f33fd0 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/ArchConfigurationRule.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/ArchConfigurationRule.java @@ -18,8 +18,8 @@ public ArchConfigurationRule resolveAdditionalDependenciesFromClassPath(boolean return this; } - public ArchConfigurationRule setFailOnEmptyShould(boolean allowEmptyShould) { - addConfigurationInitializer(() -> ArchConfiguration.get().setProperty(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME, String.valueOf(allowEmptyShould))); + public ArchConfigurationRule setFailOnEmptyShould(boolean failOnEmptyShould) { + addConfigurationInitializer(() -> ArchConfiguration.get().setProperty(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME, String.valueOf(failOnEmptyShould))); return this; } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java b/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java index a323933c1f..8cf8b73ecd 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java @@ -200,7 +200,7 @@ public static DependencyAssertion assertThatDependency(Dependency dependency) { return new DependencyAssertion(dependency); } - public static DependenciesAssertion assertThatDependencies(Iterable dependencies) { + public static DependenciesAssertion assertThatDependencies(Collection dependencies) { return new DependenciesAssertion(dependencies); } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleAssertion.java index 5484a222f2..f75c6e6aaf 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleAssertion.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleAssertion.java @@ -11,6 +11,11 @@ public ArchRuleAssertion(ArchRule rule) { super(rule, ArchRuleAssertion.class); } + public ArchRuleAssertion hasDescription(String description) { + assertThat(actual.getDescription()).isEqualTo(description); + return this; + } + public ArchRuleAssertion hasDescriptionContaining(String descriptionPart) { assertThat(actual.getDescription()).contains(descriptionPart); return this; diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java index 60e3ca63d6..5895d50fef 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ArchRuleCheckAssertion.java @@ -11,6 +11,7 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static org.assertj.core.api.Assertions.assertThat; +@SuppressWarnings("UnusedReturnValue") public class ArchRuleCheckAssertion { private final EvaluationResult evaluationResult; private final Optional error; @@ -29,6 +30,14 @@ private Optional checkRule(ArchRule rule, JavaClasses classes) { } } + public ArchRuleCheckAssertion hasViolation(String violation, Object... args) { + String expectedViolation = String.format(violation, args); + assertThat(evaluationResult.getFailureReport().getDetails()) + .as("violation details (should have some detail equal to '%s')", expectedViolation) + .anyMatch(detail -> detail.equals(expectedViolation)); + return this; + } + public ArchRuleCheckAssertion hasViolationContaining(String part, Object... args) { String expectedPart = String.format(part, args); assertThat(evaluationResult.getFailureReport().getDetails()) @@ -44,6 +53,14 @@ public ArchRuleCheckAssertion hasViolationMatching(String regex) { return this; } + public ArchRuleCheckAssertion hasNoViolationContaining(String part, Object... args) { + String expectedPart = String.format(part, args); + assertThat(evaluationResult.getFailureReport().getDetails()) + .as("violation details (should not have any detail containing '%s')", expectedPart) + .noneMatch(detail -> detail.contains(expectedPart)); + return this; + } + public ArchRuleCheckAssertion hasNoViolationMatching(String regex) { assertThat(evaluationResult.getFailureReport().getDetails()) .as("violation details (should not have any detail matching '%s')", regex) @@ -73,8 +90,11 @@ public ArchRuleCheckAssertion hasViolationWithStandardPattern(Class violating return this; } - private String toViolationMessage(Class violatingClass, String violationDescription) { - return "Class <" + violatingClass.getName() + "> " + violationDescription + " in (" + violatingClass.getSimpleName() + ".java:0)"; + @SuppressWarnings("OptionalGetWithoutIsPresent") + public ArchRuleCheckAssertion hasOnlyOneViolationContaining(String part) { + assertThat(getOnlyElement(evaluationResult.getFailureReport().getDetails())).contains(part); + assertThat(error.get().getMessage()).contains(part); + return this; } @SuppressWarnings("OptionalGetWithoutIsPresent") @@ -100,7 +120,7 @@ public ArchRuleCheckAssertion hasAnyViolationOf(String... violations) { return this; } - public ArchRuleCheckAssertion hasViolations(int numberOfViolations) { + public ArchRuleCheckAssertion hasNumberOfViolations(int numberOfViolations) { assertThat(evaluationResult.getFailureReport().getDetails()).as("number of violation").hasSize(numberOfViolations); return this; } @@ -109,4 +129,8 @@ public void hasNoViolation() { assertThat(evaluationResult.hasViolation()).as("result has violation").isFalse(); assertThat(error).as("error").isEmpty(); } + + private String toViolationMessage(Class violatingClass, String violationDescription) { + return "Class <" + violatingClass.getName() + "> " + violationDescription + " in (" + violatingClass.getSimpleName() + ".java:0)"; + } } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java index 74d07509da..78f3511721 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java @@ -1,31 +1,34 @@ package com.tngtech.archunit.testutil.assertion; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Stream; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.tngtech.archunit.base.HasDescription; import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaClass; -import org.assertj.core.api.AbstractIterableAssert; +import org.assertj.core.api.AbstractCollectionAssert; import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Lists.newArrayList; import static java.lang.System.lineSeparator; import static java.util.Arrays.stream; import static java.util.regex.Pattern.quote; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; -public class DependenciesAssertion extends AbstractIterableAssert< - DependenciesAssertion, Iterable, Dependency, DependencyAssertion> { +public class DependenciesAssertion extends AbstractCollectionAssert< + DependenciesAssertion, Collection, Dependency, DependencyAssertion> { - public DependenciesAssertion(Iterable dependencies) { + public DependenciesAssertion(Collection dependencies) { super(dependencies, DependenciesAssertion.class); } @@ -75,22 +78,14 @@ public DependenciesAssertion contain(ExpectedDependencies expectedDependencies) } public DependenciesAssertion containOnly(ExpectedDependencies expectedDependencies) { - ExpectedDependenciesMatchResult result = matchExpectedDependencies(expectedDependencies); + ExpectedDependencies.MatchResult result = matchExpectedDependencies(expectedDependencies); result.assertNoMissingDependencies(); result.assertAllDependenciesMatched(); return this; } - private ExpectedDependenciesMatchResult matchExpectedDependencies(ExpectedDependencies expectedDependencies) { - List rest = newArrayList(actual); - List missingDependencies = new ArrayList<>(); - for (ExpectedDependency expectedDependency : expectedDependencies) { - if (rest.stream().noneMatch(expectedDependency::matches)) { - missingDependencies.add(expectedDependency); - } - rest = rest.stream().filter(dependency -> !expectedDependency.matches(dependency)).collect(toList()); - } - return new ExpectedDependenciesMatchResult(missingDependencies, rest); + private ExpectedDependencies.MatchResult matchExpectedDependencies(ExpectedDependencies expectedDependencies) { + return expectedDependencies.match(actual); } public DependenciesAssertion containOnly(Class expectedOrigin, Class expectedTarget) { @@ -166,9 +161,7 @@ public ExpectedDependenciesCreator from(Class origin) { void add(Class origin, Class target, Optional descriptionTemplate) { ExpectedDependency expectedDependency = new ExpectedDependency(origin, target); - if (descriptionTemplate.isPresent()) { - expectedDependency.descriptionMatching(descriptionTemplate.get().replace("#target", quote(target.getName()))); - } + descriptionTemplate.ifPresent(s -> expectedDependency.descriptionMatching(s.replace("#target", quote(target.getName())))); expectedDependencies.add(expectedDependency); } @@ -188,6 +181,51 @@ public ExpectedDependencies inLocation(Class location, int lineNumber) { getLast(expectedDependencies).location(location, lineNumber); return this; } + + public MatchResult match(Collection actualDependencies) { + List rest = newArrayList(actualDependencies); + List missingDependencies = new ArrayList<>(); + for (ExpectedDependency expectedDependency : expectedDependencies) { + if (rest.stream().noneMatch(expectedDependency::matches)) { + missingDependencies.add(expectedDependency); + } + rest = rest.stream().filter(dependency -> !expectedDependency.matches(dependency)).collect(toList()); + } + return new MatchResult(actualDependencies, missingDependencies, rest); + } + + public static class MatchResult { + private final Collection actualDependencies; + private final Collection missingDependencies; + private final Collection unexpectedDependencies; + + private MatchResult(Collection actualDependencies, Collection missingDependencies, Collection unexpectedDependencies) { + this.actualDependencies = actualDependencies; + this.missingDependencies = missingDependencies; + this.unexpectedDependencies = unexpectedDependencies; + } + + public void assertNoMissingDependencies() { + if (!Iterables.isEmpty(missingDependencies)) { + throw new AssertionError("Could not find expected dependencies:" + lineSeparator() + + missingDependencies.stream().map(Objects::toString).collect(joining(lineSeparator())) + lineSeparator() + + "within: " + lineSeparator() + + descriptionsOf(actualDependencies).collect(joining(lineSeparator()))); + } + } + + private Stream descriptionsOf(Collection haveDescriptions) { + return haveDescriptions.stream().map(HasDescription::getDescription); + } + + public void assertAllDependenciesMatched() { + assertThat(unexpectedDependencies).as("unexpected dependencies").isEmpty(); + } + + public boolean matchesExactly() { + return unexpectedDependencies.isEmpty() && missingDependencies.isEmpty(); + } + } } private static class ExpectedDependency { @@ -224,38 +262,9 @@ public void location(Class location, int lineNumber) { @Override public String toString() { String dependency = origin.getName() + " -> " + target.getName(); - String location = locationPart.isPresent() ? " " + locationPart.get() : ""; - String description = descriptionPattern.isPresent() ? " with description matching '" + descriptionPattern.get() + "'" : ""; + String location = locationPart.map(s -> " " + s).orElse(""); + String description = descriptionPattern.map(pattern -> " with description matching '" + pattern + "'").orElse(""); return dependency + location + description; } } - - private class ExpectedDependenciesMatchResult { - private final Iterable missingDependencies; - private final Iterable unexpectedDependencies; - - private ExpectedDependenciesMatchResult(Iterable missingDependencies, Iterable unexpectedDependencies) { - this.missingDependencies = missingDependencies; - this.unexpectedDependencies = unexpectedDependencies; - } - - void assertNoMissingDependencies() { - if (!Iterables.isEmpty(missingDependencies)) { - throw new AssertionError("Could not find expected dependencies:" + lineSeparator() + Joiner.on(lineSeparator()).join(missingDependencies) - + lineSeparator() + "within: " + lineSeparator() + Joiner.on(lineSeparator()).join(descriptionsOf(actual))); - } - } - - private List descriptionsOf(Iterable haveDescriptions) { - List result = new ArrayList<>(); - for (HasDescription hasDescription : haveDescriptions) { - result.add(hasDescription.getDescription()); - } - return result; - } - - public void assertAllDependenciesMatched() { - assertThat(unexpectedDependencies).as("unexpected dependencies").isEmpty(); - } - } } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/Parameter.java b/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/Parameter.java index f9b5a13e18..d8ab78903e 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/Parameter.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/Parameter.java @@ -2,11 +2,11 @@ import static com.google.common.base.MoreObjects.toStringHelper; -class Parameter { +public class Parameter { private final Object value; private final String description; - Parameter(Object value, String description) { + public Parameter(Object value, String description) { this.value = value; this.description = description; } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/RandomSyntaxTestBase.java b/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/RandomSyntaxTestBase.java index 820e23c826..7e2626c098 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/RandomSyntaxTestBase.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/RandomSyntaxTestBase.java @@ -7,7 +7,10 @@ import java.util.List; import java.util.Random; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.IntStream; +import java.util.stream.Stream; import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; @@ -34,7 +37,10 @@ import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext; import static java.util.Arrays.asList; import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; @RunWith(DataProviderRunner.class) @@ -46,16 +52,29 @@ public abstract class RandomSyntaxTestBase { @Rule public final ArchConfigurationRule archConfigurationRule = new ArchConfigurationRule().setFailOnEmptyShould(false); - public static List> createRandomRules(RandomSyntaxSeed seed, - DescriptionReplacement... replacements) { + public static List> createRandomRules( + RandomSyntaxSeed seed, + DescriptionReplacement... replacements + ) { return createRandomRules(seed, MethodChoiceStrategy.chooseAllArchUnitSyntaxMethods(), replacements); } - public static List> createRandomRules(RandomSyntaxSeed seed, + public static List> createRandomRules( + RandomSyntaxSeed seed, MethodChoiceStrategy methodChoiceStrategy, - DescriptionReplacement... replacements) { + DescriptionReplacement... replacements + ) { + return createRandomRules(RandomRulesBlueprint.seed(seed).methodChoiceStrategy(methodChoiceStrategy).descriptionReplacements(replacements)); + } + + public static List> createRandomRules(RandomRulesBlueprint blueprint) { return IntStream.range(0, NUMBER_OF_RULES_TO_BUILD) - .mapToObj(i -> new SyntaxSpec<>(seed, methodChoiceStrategy, ExpectedDescription.from(seed, replacements))) + .mapToObj(i -> new SyntaxSpec<>( + blueprint.seed, + blueprint.methodChoiceStrategy, + blueprint.parameterProviders, + ExpectedDescription.from(blueprint.seed, blueprint.descriptionReplacements)) + ) .map(spec -> ImmutableList.of(spec.getActualArchRule(), spec.getExpectedDescription())) .collect(toList()); } @@ -87,16 +106,56 @@ private void assertCheckEitherPassesOrThrowsAssertionError(ArchRule archRule) { } } + public static class RandomRulesBlueprint implements NeedsMethodChoiceStrategy { + private final RandomSyntaxSeed seed; + private MethodChoiceStrategy methodChoiceStrategy; + private List descriptionReplacements = emptyList(); + private Set parameterProviders = emptySet(); + + private RandomRulesBlueprint(RandomSyntaxSeed seed) { + this.seed = seed; + } + + @Override + public RandomRulesBlueprint methodChoiceStrategy(MethodChoiceStrategy methodChoiceStrategy) { + this.methodChoiceStrategy = methodChoiceStrategy; + return this; + } + + public RandomRulesBlueprint descriptionReplacements(DescriptionReplacement... descriptionReplacements) { + this.descriptionReplacements = stream(descriptionReplacements).collect(toList()); + return this; + } + + public RandomRulesBlueprint parameterProviders(SingleParameterProvider... parameterProviders) { + this.parameterProviders = stream(parameterProviders).collect(toSet()); + return this; + } + + public static NeedsMethodChoiceStrategy seed(RandomSyntaxSeed seed) { + return new RandomRulesBlueprint(seed); + } + } + + public interface NeedsMethodChoiceStrategy { + RandomRulesBlueprint methodChoiceStrategy(MethodChoiceStrategy methodChoiceStrategy); + } + private static class SyntaxSpec { private static final int MAX_STEPS = 50; private final ExpectedDescription expectedDescription; private final ArchRule actualArchRule; - SyntaxSpec(RandomSyntaxSeed seed, MethodChoiceStrategy methodChoiceStrategy, ExpectedDescription expectedDescription) { + SyntaxSpec( + RandomSyntaxSeed seed, + MethodChoiceStrategy methodChoiceStrategy, + Set singleParameterProviders, + ExpectedDescription expectedDescription + ) { this.expectedDescription = expectedDescription; MethodCallChain methodCallChain = new MethodCallChain(methodChoiceStrategy, new TypedValue(seed.getType(), seed.getValue())); - Step firstStep = new PartialStep(expectedDescription, methodCallChain); + Step firstStep = new PartialStep(expectedDescription, methodCallChain, singleParameterProviders); LOG.debug("Starting from {}", firstStep); try { LastStep result = firstStep.continueSteps(0, MAX_STEPS); @@ -131,11 +190,11 @@ private static class ExpectedDescription { private final List descriptionReplacements; private final List description = new ArrayList<>(); - private ExpectedDescription(DescriptionReplacement[] descriptionReplacements) { - this.descriptionReplacements = ImmutableList.copyOf(descriptionReplacements); + private ExpectedDescription(List descriptionReplacements) { + this.descriptionReplacements = descriptionReplacements; } - public static ExpectedDescription from(RandomSyntaxSeed seed, DescriptionReplacement[] patternsToExclude) { + public static ExpectedDescription from(RandomSyntaxSeed seed, List patternsToExclude) { return new ExpectedDescription(patternsToExclude).add(seed.getDescription()); } @@ -190,27 +249,21 @@ private Step(ExpectedDescription expectedDescription, MethodCallChain methodCall private static class PartialStep extends Step { private static final int LOW_NUMBER_OF_LEFT_STEPS = 5; - private static final ParameterProvider parameterProvider = new ParameterProvider(); - final Parameters parameters; + private final ParameterProvider parameterProvider; + private final Parameters parameters; - PartialStep(ExpectedDescription expectedDescription, MethodCallChain methodCallChain) { - this(expectedDescription, methodCallChain, getParametersFor(methodCallChain.getNextMethodCandidate())); + PartialStep(ExpectedDescription expectedDescription, MethodCallChain methodCallChain, Set singleParameterProviders) { + this(expectedDescription, methodCallChain, new ParameterProvider(singleParameterProviders)); } - private PartialStep( - ExpectedDescription expectedDescription, - MethodCallChain methodCallChain, - Parameters parameters) { - + PartialStep(ExpectedDescription expectedDescription, MethodCallChain methodCallChain, ParameterProvider parameterProvider) { super(expectedDescription, methodCallChain); - this.parameters = parameters; - expectedDescription.add(getDescription()); - } - - private static Parameters getParametersFor(Method method) { + this.parameterProvider = parameterProvider; + Method method = methodCallChain.getNextMethodCandidate(); List> tokens = stream(method.getGenericParameterTypes()).map(TypeToken::of).collect(toList()); - return parameterProvider.get(method.getName(), tokens); + this.parameters = parameterProvider.get(method.getName(), tokens); + expectedDescription.add(getDescription()); } @Override @@ -226,7 +279,7 @@ LastStep continueSteps(int currentStepCount, int maxSteps) { boolean shouldContinue = methodCallChain.hasAnotherMethodCandidate() && shouldContinue(methodCallChain.getCurrentValue(), lowNumberOfStepsLeft); Step nextStep = shouldContinue - ? new PartialStep(expectedDescription, methodCallChain) + ? new PartialStep(expectedDescription, methodCallChain, parameterProvider) : new LastStep(expectedDescription, methodCallChain); LOG.debug("Next step is {}", nextStep); @@ -286,10 +339,10 @@ public String toString() { } private static class ParameterProvider { - private static final Set singleParameterProviders = ImmutableSet.builder() - .add(new SpecificParameterProvider(String.class) { + private static final Set defaultSingleParameterProviders = ImmutableSet.builder() + .add(new SingleParameterProvider(String.class) { @Override - Parameter get(String methodName, TypeToken type) { + public Parameter get(String methodName, TypeToken type) { if (methodName.toLowerCase().contains("annotat")) { return new Parameter("AnnotationType", "@AnnotationType"); } else if (methodName.toLowerCase().contains("assign") @@ -304,17 +357,28 @@ Parameter get(String methodName, TypeToken type) { } } }) - .add(new SpecificParameterProvider(String[].class) { + .add(new SingleParameterProvider(String[].class) { @Override - Parameter get(String methodName, TypeToken type) { + public Parameter get(String methodName, TypeToken type) { return methodName.toLowerCase().contains("type") ? new Parameter(new String[]{"first.Type", "second.Type"}, "[first.Type, second.Type]") : new Parameter(new String[]{"one", "two"}, "['one', 'two']"); } }) - .add(new SpecificParameterProvider(Class.class) { + .add(new SingleParameterProvider(Object[].class) { + @Override + public Parameter get(String methodName, TypeToken type) { + return new Parameter(new Object[]{"one", "two"}, "[one, two]"); + } + + @Override + protected boolean canHandle(String methodName, Class type) { + return supportedType == type; // only use this when the type is really Object[] and not for more specific subtypes + } + }) + .add(new SingleParameterProvider(Class.class) { @Override - Parameter get(String methodName, TypeToken type) { + public Parameter get(String methodName, TypeToken type) { TypeToken typeParam = type.resolveType(Class.class.getTypeParameters()[0]); String description = Annotation.class.isAssignableFrom(typeParam.getRawType()) ? "@" + Deprecated.class.getSimpleName() @@ -322,29 +386,29 @@ Parameter get(String methodName, TypeToken type) { return new Parameter(Deprecated.class, description); } }) - .add(new SpecificParameterProvider(Class[].class) { + .add(new SingleParameterProvider(Class[].class) { @Override - Parameter get(String methodName, TypeToken type) { + public Parameter get(String methodName, TypeToken type) { Class[] value = {String.class, Serializable.class}; return new Parameter(value, "[" + value[0].getName() + ", " + value[1].getName() + "]"); } }) - .add(new SpecificParameterProvider(Enum.class) { + .add(new SingleParameterProvider(Enum.class) { @Override - Parameter get(String methodName, TypeToken type) { + public Parameter get(String methodName, TypeToken type) { Object constant = type.getRawType().getEnumConstants()[0]; return new Parameter(constant, String.valueOf(constant)); } }) - .add(new SpecificParameterProvider(DescribedPredicate.class) { + .add(new SingleParameterProvider(DescribedPredicate.class) { @Override - Parameter get(String methodName, TypeToken type) { + public Parameter get(String methodName, TypeToken type) { return new Parameter(DescribedPredicate.alwaysTrue().as("custom predicate"), "custom predicate"); } }) - .add(new SpecificParameterProvider(ArchCondition.class) { + .add(new SingleParameterProvider(ArchCondition.class) { @Override - Parameter get(String methodName, TypeToken type) { + public Parameter get(String methodName, TypeToken type) { return new Parameter(new ArchCondition("overrideMe") { @Override public void check(Object item, ConditionEvents events) { @@ -354,16 +418,22 @@ public void check(Object item, ConditionEvents events) { }) .build(); - private final List parametersProvider = ImmutableList.of( + private final List singleParameterProviders; + + private final List parametersProvider = ImmutableList.of( new FieldMethodParametersProvider(), new CallMethodClassParametersProvider(), new CallMethodStringParametersProvider(), new CallConstructorClassParametersProvider(), new CallConstructorStringParametersProvider(), - new SingleParametersProvider()); + new DefaultParametersProvider()); + + public ParameterProvider(Set additionalParameterProviders) { + singleParameterProviders = Stream.concat(additionalParameterProviders.stream(), defaultSingleParameterProviders.stream()).collect(toList()); + } Parameters get(String methodName, List> types) { - for (SpecificParametersProvider provider : parametersProvider) { + for (ParametersProvider provider : parametersProvider) { if (provider.canHandle(methodName, types)) { return provider.get(methodName, types); } @@ -373,35 +443,21 @@ Parameters get(String methodName, List> types) { } Parameter get(String methodName, TypeToken type) { - for (SpecificParameterProvider provider : singleParameterProviders) { - if (provider.canHandle(type.getRawType())) { + for (SingleParameterProvider provider : singleParameterProviders) { + if (provider.canHandle(methodName, type.getRawType())) { return provider.get(methodName, type); } } - throw new RuntimeException("Parameter type " + type + " is not supported yet"); + throw new RuntimeException("Parameter type " + type + " of method " + methodName + " is not supported yet"); } - private abstract static class SpecificParametersProvider { + private abstract static class ParametersProvider { abstract boolean canHandle(String methodName, List> parameterTypes); abstract Parameters get(String methodName, List> parameterTypes); } - private abstract static class SpecificParameterProvider { - private final Class supportedType; - - SpecificParameterProvider(Class supportedType) { - this.supportedType = supportedType; - } - - boolean canHandle(Class type) { - return supportedType.isAssignableFrom(type); - } - - abstract Parameter get(String methodName, TypeToken type); - } - - private class FieldMethodParametersProvider extends SpecificParametersProvider { + private class FieldMethodParametersProvider extends ParametersProvider { @Override boolean canHandle(String methodName, List> parameterTypes) { return methodName.toLowerCase().contains("field"); @@ -409,7 +465,7 @@ boolean canHandle(String methodName, List> parameterTypes) { @Override Parameters get(String methodName, List> parameterTypes) { - Parameters parameters = new SingleParametersProvider().get(methodName, parameterTypes); + Parameters parameters = new DefaultParametersProvider().get(methodName, parameterTypes); if (parameterTypes.size() == 2) { return specificHandlingOfTwoParameterMethods(methodName, parameterTypes, parameters); } @@ -475,7 +531,7 @@ private boolean firstParameterTypeMatches(List> parameterTypes) { } } - private abstract static class CallCodeUnitParametersProvider extends SpecificParametersProvider { + private abstract static class CallCodeUnitParametersProvider extends ParametersProvider { private final CanHandlePredicate predicate; CallCodeUnitParametersProvider(CanHandlePredicate predicate) { @@ -495,7 +551,7 @@ private class CallMethodClassParametersProvider extends CallCodeUnitParametersPr @Override Parameters get(String methodName, List> parameterTypes) { - Parameters parameters = new SingleParametersProvider().get(methodName, parameterTypes); + Parameters parameters = new DefaultParametersProvider().get(methodName, parameterTypes); String params = createCallDetailsForClassArrayAtIndex(2, parameters.get(1).getValue(), parameters); return parameters.withDescription(params); } @@ -508,7 +564,7 @@ private class CallMethodStringParametersProvider extends CallCodeUnitParametersP @Override Parameters get(String methodName, List> parameterTypes) { - Parameters parameters = new SingleParametersProvider().get(methodName, parameterTypes); + Parameters parameters = new DefaultParametersProvider().get(methodName, parameterTypes); String params = createCallDetailsForStringArrayAtIndex(2, parameters.get(1).getValue(), parameters); return parameters.withDescription(params); } @@ -521,7 +577,7 @@ private class CallConstructorClassParametersProvider extends CallCodeUnitParamet @Override Parameters get(String methodName, List> parameterTypes) { - Parameters parameters = new SingleParametersProvider().get(methodName, parameterTypes); + Parameters parameters = new DefaultParametersProvider().get(methodName, parameterTypes); String params = createCallDetailsForClassArrayAtIndex(1, CONSTRUCTOR_NAME, parameters); return parameters.withDescription(params); } @@ -534,7 +590,7 @@ private class CallConstructorStringParametersProvider extends CallCodeUnitParame @Override Parameters get(String methodName, List> parameterTypes) { - Parameters parameters = new SingleParametersProvider().get(methodName, parameterTypes); + Parameters parameters = new DefaultParametersProvider().get(methodName, parameterTypes); String params = createCallDetailsForStringArrayAtIndex(1, CONSTRUCTOR_NAME, parameters); return parameters.withDescription(params); } @@ -559,7 +615,7 @@ private String createCallDetails(Object callTargetName, List simpleParam Joiner.on(", ").join(simpleParamTypeNames)); } - private class SingleParametersProvider extends SpecificParametersProvider { + private class DefaultParametersProvider extends ParametersProvider { @Override boolean canHandle(String methodName, List> parameterTypes) { return true; @@ -602,4 +658,30 @@ public String toString() { return getClass().getSimpleName() + "{/" + search + "/" + replacement + "/}"; } } + + protected static class ReplaceEverythingSoFar implements DescriptionReplacement { + private final Pattern pattern; + private final String replaceWith; + + public ReplaceEverythingSoFar(String pattern, String replaceWith) { + this.pattern = Pattern.compile(pattern); + this.replaceWith = replaceWith; + } + + @Override + public boolean applyTo(String currentToken, List currentDescription) { + Matcher matcher = pattern.matcher(currentToken); + if (matcher.matches()) { + currentDescription.clear(); + currentDescription.add(matcher.replaceAll(replaceWith)); + return true; + } + return false; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{/" + pattern + "/" + replaceWith + "/}"; + } + } } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/SingleParameterProvider.java b/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/SingleParameterProvider.java new file mode 100644 index 0000000000..6011cbb6d3 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/syntax/SingleParameterProvider.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.testutil.syntax; + +import com.google.common.reflect.TypeToken; + +public abstract class SingleParameterProvider { + protected final Class supportedType; + + public SingleParameterProvider(Class supportedType) { + this.supportedType = supportedType; + } + + protected boolean canHandle(String methodName, Class type) { + return supportedType.isAssignableFrom(type); + } + + public abstract Parameter get(String methodName, TypeToken type); +} diff --git a/buildSrc/src/main/resources/code_quality/spotbugs-excludes.xml b/buildSrc/src/main/resources/code_quality/spotbugs-excludes.xml index 82d9d4d171..0df9b98438 100644 --- a/buildSrc/src/main/resources/code_quality/spotbugs-excludes.xml +++ b/buildSrc/src/main/resources/code_quality/spotbugs-excludes.xml @@ -53,7 +53,7 @@ - + diff --git a/docs/userguide/008_The_Library_API.adoc b/docs/userguide/008_The_Library_API.adoc index d32b90a742..1c706e6a63 100644 --- a/docs/userguide/008_The_Library_API.adoc +++ b/docs/userguide/008_The_Library_API.adoc @@ -115,10 +115,9 @@ note right on link #crimson: one adapter must not know\nabout any other adapter ---- - === Slices -Currently there are two "slice" rules offered by the Library API. These are basically rules +Currently, there are two "slice" rules offered by the Library API. These are basically rules that slice the code by packages, and contain assertions on those slices. The entrance point is: [source,java,options="nowrap"] @@ -197,6 +196,108 @@ cycles.maxNumberToDetect=50 cycles.maxNumberOfDependenciesPerEdge=5 ---- +==== The Cycle Detection Core API + +The underlying infrastructure for cycle detection that the `slices()` rule makes use of can also be accessed +without any rule syntax around it. This allows to use the pure cycle detection algorithm in custom +checks or libraries. The core class of the cycle detection is + +[source,java,options="nowrap"] +---- +com.tngtech.archunit.library.cycle_detection.CycleDetector +---- + +It can be used on a set of a generic type `NODE` in combination with a generic `Set` +(where `EDGE implements Edge`) representing the edges of the graph: + +[source,java,options="nowrap"] +---- +Set nodes = // ... +Set> edges = // ... +Cycles> foundCycles = CycleDetector.detectCycles(nodes, edges); +---- + +Edges are parameterized by a generic type `EDGE` to allow custom edge types that can +then transport additional meta-information if needed. + + +=== Modularization Rules + +[NOTE] +Note: ArchUnit doesn't strive to be a "competition" for module systems like the +Java Platform Module System. Such systems have advantages like checks at compile time +versus test time as ArchUnit does. So, if another module system works well in your +environment, there is no need to switch over. But ArchUnit can bring JPMS-like features +to older code bases, e.g. Java 8 projects, or environments where the JPMS is for some +reason no option. It also can accompany a module system by adding additional rules e.g. +on the API of a module. + +To express the concept of modularization ArchUnit offers `ArchModule`s. The entrypoint into +the API is `ModuleRuleDefinition`, e.g. + +[source,java,options="nowrap"] +---- +ModuleRuleDefinition.modules().definedByPackages("..example.(*)..").should().beFreeOfCycles(); +---- + +As the example shows, it shares some concepts with the <> API. For example `definedByPackages(..)` +follows the same semantics as `slices().matching(..)`. +Also, the configuration options for cycle detection mentioned in the last section are shared by these APIs. +But, it also offers several powerful concepts beyond that API to express many different modularization scenarios. + +One example would be to express modules via annotation. We can introduce a custom annotation +like `@AppModule` and follow a convention to annotate the top-level `package-info` file +of each package we consider the root of a module. E.g. + +[source,java,options="nowrap"] +.com/myapp/example/module_one/package-info.java +---- +@AppModule( + name = "Module One", + allowedDependencies = {"Module Two", "Module Three"}, + exposedPackages = {"..module_one.api.."} +) +package com.myapp.example.module_one; +---- + +We can then define a rule using this annotation: + +[source,java,options="nowrap"] +---- +modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", + consideringOnlyDependenciesInAnyPackage("..example..")) + .andShould().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages") +---- + +As the example shows, the syntax carries on meta-information (like the annotation of the annotated +`package-info`) into the created `ArchModule` objects where it can +be used to define the rule. In this example, the allowed dependencies are taken from the `@AppModule` +annotation on the respective `package-info` and compared to the actual module dependencies. Any +dependency not listed is reported as violation. +Likewise, the exposed packages are taken from the `@AppModule` annotation and any dependency +where the target class's package doesn't match any declared package identifier is reported +as violation. + +Note that the `modules()` API can be adjusted in many ways to model custom requirements. +For further details, please take a look at the examples provided +https://github.com/TNG/ArchUnit-Examples/blob/main/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java[here]. + +==== Modularization Core API + +The infrastructure to create modules and inspect their dependencies can also be used outside +the rule syntax, e.g. for custom checks or utility code: + +[source,java,options="nowrap"] +---- +ArchModules modules = ArchModules.defineByPackages("..example.(*)..").modularize(javaClasses); +ArchModule coreModule = modules.getByIdentifier("core"); +Set> coreDependencies = coreModule.getModuleDependenciesFromSelf(); +coreDependencies.forEach(...); +---- + + === General Coding Rules The Library API also offers a small set of coding rules that might be useful in various projects.