diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java index c070cbee99..ef94dfed65 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/OnionArchitectureTest.java @@ -1,7 +1,17 @@ package com.tngtech.archunit.exampletest.junit4; +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem; import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.ArchUnitRunner; @@ -9,11 +19,17 @@ import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @Category(Example.class) @RunWith(ArchUnitRunner.class) -@AnalyzeClasses(packages = "com.tngtech.archunit.example.onionarchitecture") +@AnalyzeClasses(packages = { + "com.tngtech.archunit.example.onionarchitecture", + "com.tngtech.archunit.example.onionarchitecture_by_annotations" +}) public class OnionArchitectureTest { @ArchTest @@ -35,4 +51,30 @@ public class OnionArchitectureTest { .adapter("rest", "..adapter.rest..") .ignoreDependency(OrderItem.class, OrderQuantity.class); + + @ArchTest + static final ArchRule onion_architecture_defined_by_annotations = onionArchitecture() + .domainModels(byAnnotation(DomainModel.class)) + .domainServices(byAnnotation(DomainService.class)) + .applicationServices(byAnnotation(Application.class)) + .adapter("cli", byAnnotation(adapter("cli"))) + .adapter("persistence", byAnnotation(adapter("persistence"))) + .adapter("rest", byAnnotation(adapter("rest"))); + + private static DescribedPredicate byAnnotation(Class annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate byAnnotation(DescribedPredicate> annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate> adapter(String adapterName) { + return describe( + String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName), + a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName) + ); + } } diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java index 8012b95cdb..60ee3cbbbd 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/OnionArchitectureTest.java @@ -1,16 +1,32 @@ package com.tngtech.archunit.exampletest.junit5; +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem; import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; 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 static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @ArchTag("example") -@AnalyzeClasses(packages = "com.tngtech.archunit.example.onionarchitecture") +@AnalyzeClasses(packages = { + "com.tngtech.archunit.example.onionarchitecture", + "com.tngtech.archunit.example.onionarchitecture_by_annotations" +}) public class OnionArchitectureTest { @ArchTest @@ -32,4 +48,30 @@ public class OnionArchitectureTest { .adapter("rest", "..adapter.rest..") .ignoreDependency(OrderItem.class, OrderQuantity.class); + + @ArchTest + static final ArchRule onion_architecture_defined_by_annotations = onionArchitecture() + .domainModels(byAnnotation(DomainModel.class)) + .domainServices(byAnnotation(DomainService.class)) + .applicationServices(byAnnotation(Application.class)) + .adapter("cli", byAnnotation(adapter("cli"))) + .adapter("persistence", byAnnotation(adapter("persistence"))) + .adapter("rest", byAnnotation(adapter("rest"))); + + private static DescribedPredicate byAnnotation(Class annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate byAnnotation(DescribedPredicate> annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate> adapter(String adapterName) { + return describe( + String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName), + a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName) + ); + } } diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Adapter.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Adapter.java new file mode 100644 index 0000000000..513588690f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Adapter.java @@ -0,0 +1,5 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface Adapter { + String value(); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Application.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Application.java new file mode 100644 index 0000000000..79d7897740 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Application.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface Application { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainModel.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainModel.java new file mode 100644 index 0000000000..0caca4939f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainModel.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface DomainModel { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainService.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainService.java new file mode 100644 index 0000000000..cdffb95c64 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainService.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations; + +public @interface DomainService { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/AdministrationPort.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/AdministrationPort.java new file mode 100644 index 0000000000..d73731730e --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/AdministrationPort.java @@ -0,0 +1,8 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; + +@Application +public interface AdministrationPort { + T getInstanceOf(Class type); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/ShoppingApplication.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/ShoppingApplication.java new file mode 100644 index 0000000000..6562cf0068 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/ShoppingApplication.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; + +@Application +public class ShoppingApplication { + public static void main(String[] args) { + // start the whole application / provide IOC features + } + + public static AdministrationPort openAdministrationPort() { + return new AdministrationPort() { + @Override + public T getInstanceOf(Class type) { + throw new UnsupportedOperationException("Not yet implemented"); + } + }; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/administration/AdministrationCLI.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/administration/AdministrationCLI.java new file mode 100644 index 0000000000..66ffecd482 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/administration/AdministrationCLI.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.administration; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.AdministrationPort; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.ShoppingApplication; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductRepository; + +@Adapter("cli") +@SuppressWarnings("unused") +public class AdministrationCLI { + public static void main(String[] args) { + AdministrationPort port = ShoppingApplication.openAdministrationPort(); + handle(args, port); + } + + private static void handle(String[] args, AdministrationPort port) { + // violates the pairwise independence of adapters + ProductRepository repository = port.getInstanceOf(ProductRepository.class); + long count = repository.getTotalCount(); + // parse arguments and re-configure application according to count through port + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderItem.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderItem.java new file mode 100644 index 0000000000..015b11d2d9 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderItem.java @@ -0,0 +1,26 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.Product; + +@DomainModel +public class OrderItem { + private final Product product; + private final OrderQuantity quantity; + + public OrderItem(Product product, OrderQuantity quantity) { + if (product == null) { + throw new IllegalArgumentException("Product must not be null"); + } + if (quantity == null) { + throw new IllegalArgumentException("Quantity not be null"); + } + this.product = product; + this.quantity = quantity; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{product=" + product + ", quantity=" + quantity + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderQuantity.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderQuantity.java new file mode 100644 index 0000000000..4e9e4226a8 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderQuantity.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; + +@DomainService +@SuppressWarnings("unused") +public class OrderQuantity { + private final int quantity; + + public OrderQuantity(int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException("Quantity must be positive"); + } + this.quantity = quantity; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{quantity=" + quantity + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/PaymentMethod.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/PaymentMethod.java new file mode 100644 index 0000000000..3e27ac4a2c --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/PaymentMethod.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; + +@DomainModel +public class PaymentMethod { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/Product.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/Product.java new file mode 100644 index 0000000000..394373a288 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/Product.java @@ -0,0 +1,27 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; + +@DomainModel +public class Product { + // Dependency on ProductId violates the architecture, since ProductId resides with persistence adapter + private final ProductId id; + // Dependency on ProductName violates the architecture, since ProductName is located in the DomainService layer + private final ProductName name; + + public Product(ProductId id, ProductName name) { + if (id == null) { + throw new IllegalArgumentException("Product id must not be null"); + } + if (name == null) { + throw new IllegalArgumentException("Product name must not be null"); + } + this.id = id; + this.name = name; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + ", name=" + name + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductId.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductId.java new file mode 100644 index 0000000000..5e38d7e0c7 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductId.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +@SuppressWarnings("unused") +public class ProductId { + private final UUID id; + + public ProductId(UUID id) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + this.id = id; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductJpaRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductJpaRepository.java new file mode 100644 index 0000000000..d05b8aea2f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductJpaRepository.java @@ -0,0 +1,17 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +@SuppressWarnings("unused") +public class ProductJpaRepository implements ProductRepository { + @Override + public Product read(ProductId id) { + return new Product(id, new ProductName("would normally be read")); + } + + @Override + public long getTotalCount() { + return 0; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductName.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductName.java new file mode 100644 index 0000000000..5cddb618cc --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductName.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; + +@DomainService +@SuppressWarnings("unused") +public class ProductName { + private final String name; + + public ProductName(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name must not be empty"); + } + this.name = name; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{name='" + name + '\'' + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductRepository.java new file mode 100644 index 0000000000..097cba1e0c --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductRepository.java @@ -0,0 +1,11 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +// Violates the architecture because Domain must be the owner of the interfaces, not the persistence adapter +@Adapter("persistence") +public interface ProductRepository { + Product read(ProductId id); + + long getTotalCount(); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCart.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCart.java new file mode 100644 index 0000000000..e88bd32c9e --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCart.java @@ -0,0 +1,32 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import java.util.HashSet; +import java.util.Set; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderItem; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.PaymentMethod; + +@DomainModel +@SuppressWarnings("unused") +public class ShoppingCart { + private final ShoppingCartId id; + private final Set orderItems = new HashSet<>(); + + public ShoppingCart(ShoppingCartId id) { + this.id = id; + } + + public void add(OrderItem orderItem) { + orderItems.add(orderItem); + } + + public void executeOrder(PaymentMethod method) { + // complete financial transaction and initiate shipping process + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + ", orderItems=" + orderItems + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartId.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartId.java new file mode 100644 index 0000000000..b4cff159a4 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartId.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +public class ShoppingCartId { + private final UUID id; + + public ShoppingCartId(UUID id) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + this.id = id; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{id=" + id + '}'; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartJpaRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartJpaRepository.java new file mode 100644 index 0000000000..89cff6865d --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartJpaRepository.java @@ -0,0 +1,18 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +@Adapter("persistence") +@SuppressWarnings("unused") +public class ShoppingCartJpaRepository implements ShoppingCartRepository { + @Override + public ShoppingCart read(ShoppingCartId id) { + // would normally load fully initialized shopping cart + return new ShoppingCart(id); + } + + @Override + public void save(ShoppingCart shoppingCart) { + // store shopping cart via JPA + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartRepository.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartRepository.java new file mode 100644 index 0000000000..9ea700e10b --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartRepository.java @@ -0,0 +1,11 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; + +// Violates the architecture because Domain must be the owner of the interfaces, not the persistence adapter +@Adapter("persistence") +public interface ShoppingCartRepository { + ShoppingCart read(ShoppingCartId id); + + void save(ShoppingCart shoppingCart); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingController.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingController.java new file mode 100644 index 0000000000..8dfeff5f80 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingController.java @@ -0,0 +1,22 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductId; + +@Adapter("rest") +@SuppressWarnings("unused") +public class ShoppingController { + private final ShoppingService shoppingService; + + public ShoppingController(ShoppingService shoppingService) { + this.shoppingService = shoppingService; + } + + // @POST or similar + public void addToShoppingCart(UUID shoppingCartId, UUID productId, int quantity) { + shoppingService.addToShoppingCart(new ShoppingCartId(shoppingCartId), new ProductId(productId), new OrderQuantity(quantity)); + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingService.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingService.java new file mode 100644 index 0000000000..11a74616cc --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingService.java @@ -0,0 +1,27 @@ +package com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderItem; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.Product; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductId; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductRepository; + +@DomainService +public class ShoppingService { + private final ShoppingCartRepository shoppingCartRepository; + private final ProductRepository productRepository; + + public ShoppingService(ShoppingCartRepository shoppingCartRepository, ProductRepository productRepository) { + this.shoppingCartRepository = shoppingCartRepository; + this.productRepository = productRepository; + } + + public void addToShoppingCart(ShoppingCartId shoppingCartId, ProductId productId, OrderQuantity quantity) { + ShoppingCart shoppingCart = shoppingCartRepository.read(shoppingCartId); + Product product = productRepository.read(productId); + OrderItem newItem = new OrderItem(product, quantity); + shoppingCart.add(newItem); + shoppingCartRepository.save(shoppingCart); + } +} diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java index 3e17bd72dd..026551d002 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/OnionArchitectureTest.java @@ -1,17 +1,32 @@ package com.tngtech.archunit.exampletest; +import java.lang.annotation.Annotation; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.example.onionarchitecture.domain.model.OrderItem; import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Adapter; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.Application; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainModel; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.annotations.DomainService; import org.junit.Test; import org.junit.experimental.categories.Category; +import static com.tngtech.archunit.base.DescribedPredicate.describe; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @Category(Example.class) public class OnionArchitectureTest { - private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example.onionarchitecture"); + private final JavaClasses classes = new ClassFileImporter().importPackages( + "com.tngtech.archunit.example.onionarchitecture", + "com.tngtech.archunit.example.onionarchitecture_by_annotations"); @Test public void onion_architecture_is_respected() { @@ -39,4 +54,33 @@ public void onion_architecture_is_respected_with_exception() { .check(classes); } + + @Test + public void onion_architecture_defined_by_annotations() { + onionArchitecture() + .domainModels(byAnnotation(DomainModel.class)) + .domainServices(byAnnotation(DomainService.class)) + .applicationServices(byAnnotation(Application.class)) + .adapter("cli", byAnnotation(adapter("cli"))) + .adapter("persistence", byAnnotation(adapter("persistence"))) + .adapter("rest", byAnnotation(adapter("rest"))) + .check(classes); + } + + private static DescribedPredicate byAnnotation(Class annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate byAnnotation(DescribedPredicate> annotationType) { + DescribedPredicate annotatedWith = annotatedWith(annotationType); + return belongTo(annotatedWith).as(annotatedWith.getDescription()); + } + + private static DescribedPredicate> adapter(String adapterName) { + return describe( + String.format("@%s(\"%s\")", Adapter.class.getSimpleName(), adapterName), + a -> a.getRawType().isEquivalentTo(Adapter.class) && a.as(Adapter.class).value().equals(adapterName) + ); + } } 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 d405a7ea6c..b1364a1d85 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 @@ -1099,6 +1099,8 @@ Stream OnionArchitectureTest() { addExpectedCommonFailure.accept("onion_architecture_is_respected_with_exception", expectedTestFailures); + ExpectedOnionArchitectureByAnnotationFailures.addTo(expectedTestFailures); + return expectedTestFailures.toDynamicTests(); } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java new file mode 100644 index 0000000000..8099ce0eb9 --- /dev/null +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java @@ -0,0 +1,77 @@ +package com.tngtech.archunit.integration; + +import java.util.UUID; + +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.AdministrationPort; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.administration.AdministrationCLI; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderItem; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.order.OrderQuantity; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.Product; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductId; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductName; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.product.ProductRepository; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingCart; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingCartId; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingCartRepository; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingController; +import com.tngtech.archunit.example.onionarchitecture_by_annotations.onion.shopping.ShoppingService; +import com.tngtech.archunit.testutils.ExpectedTestFailures; + +import static com.tngtech.archunit.testutils.ExpectedAccess.callFromMethod; +import static com.tngtech.archunit.testutils.ExpectedDependency.constructor; +import static com.tngtech.archunit.testutils.ExpectedDependency.field; +import static com.tngtech.archunit.testutils.ExpectedDependency.method; +import static java.lang.System.lineSeparator; + +class ExpectedOnionArchitectureByAnnotationFailures { + // This is only extracted to avoid the import clashes. + // Otherwise, it would be really bloated two write down with fully qualified class names everywhere + static void addTo(ExpectedTestFailures expectedTestFailures) { + expectedTestFailures + .ofRule("onion_architecture_defined_by_annotations", + "Onion architecture consisting of" + lineSeparator() + + "domain models (annotated with @DomainModel)" + lineSeparator() + + "domain services (annotated with @DomainService)" + lineSeparator() + + "application services (annotated with @Application)" + lineSeparator() + + "adapter 'cli' (annotated with @Adapter(\"cli\"))" + lineSeparator() + + "adapter 'persistence' (annotated with @Adapter(\"persistence\"))" + lineSeparator() + + "adapter 'rest' (annotated with @Adapter(\"rest\"))") + .by(constructor(Product.class).withParameter(ProductId.class)) + .by(constructor(Product.class).withParameter(ProductName.class)) + .by(constructor(ShoppingCart.class).withParameter(ShoppingCartId.class)) + .by(constructor(ShoppingService.class).withParameter(ProductRepository.class)) + .by(constructor(ShoppingService.class).withParameter(ShoppingCartRepository.class)) + + .by(field(Product.class, "id").ofType(ProductId.class)) + .by(field(Product.class, "name").ofType(ProductName.class)) + .by(field(ShoppingCart.class, "id").ofType(ShoppingCartId.class)) + .by(field(ShoppingService.class, "productRepository").ofType(ProductRepository.class)) + .by(field(ShoppingService.class, "shoppingCartRepository").ofType(ShoppingCartRepository.class)) + + .by(method(AdministrationCLI.class, "handle") + .referencingClassObject(ProductRepository.class) + .inLine(18)) + .by(callFromMethod(AdministrationCLI.class, "handle", String[].class, AdministrationPort.class) + .toMethod(ProductRepository.class, "getTotalCount") + .inLine(19).asDependency()) + .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) + .toConstructor(ProductId.class, UUID.class) + .inLine(20).asDependency()) + .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) + .toConstructor(ShoppingCartId.class, UUID.class) + .inLine(20).asDependency()) + .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ProductId.class)) + .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ShoppingCartId.class)) + .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) + .toMethod(ShoppingCartRepository.class, "read", ShoppingCartId.class) + .inLine(21).asDependency()) + .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) + .toMethod(ProductRepository.class, "read", ProductId.class) + .inLine(22).asDependency()) + .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) + .toMethod(ShoppingCartRepository.class, "save", ShoppingCart.class) + .inLine(25).asDependency()) + .by(constructor(OrderItem.class).withParameter(OrderQuantity.class)) + .by(field(OrderItem.class, "quantity").ofType(OrderQuantity.class)); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java index 6c2e53e8f1..cebb90c89a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java @@ -2357,13 +2357,48 @@ public static DescribedPredicate equivalentTo(final Class clazz) { * A predicate to determine if a {@link JavaClass} "belongs" to one of the passed {@link Class classes}, * where we define "belong" as being equivalent to the class itself or any inner/anonymous class of this class. * - * @param classes The {@link Class classes} to check the {@link JavaClass} against + * @param classes The {@link Class classes} to check the {@link JavaClass} and its enclosing classes against * @return A {@link DescribedPredicate} returning true, if and only if the tested {@link JavaClass} is equivalent to - * one of the supplied {@link Class classes} or to one of its inner/anonymous classes. + * one of the supplied {@link Class classes} or is a nested/inner/anonymous class of one of those classes. + * + * @see #belongTo(DescribedPredicate) */ @PublicAPI(usage = ACCESS) public static DescribedPredicate belongToAnyOf(Class... classes) { - return new BelongToAnyOfPredicate(classes); + return belongTo(DescribedPredicate.describe( + "any of " + formatNamesOf(classes), + javaClass -> stream(classes).anyMatch(javaClass::isEquivalentTo) + )); + } + + /** + * A predicate to determine if a {@link JavaClass} "belongs" to a class matching the given predicate, + * where we define "belong" as being equivalent to the class itself or any inner/anonymous class of this class. + *

+ * For example {@code belongTo(annotatedWith(Something.class))} would apply to the following cases + *

+         *{@literal @}Something
+         * class Outer {
+         *     // Inner would match belongTo(annotatedWith(Something.class))
+         *     class Inner {}
+         *
+         *     void call() {
+         *         // this anonymous class would also match belongTo(annotatedWith(Something.class))
+         *         new Serializable() {}
+         *     }
+         * }
+         *
+         * // this class would not match, since it does not belong to a class annotated with @Something
+         * class Other {}
+         * 
+ * + * @param predicate The {@link DescribedPredicate predicate} to check the {@link JavaClass} and enclosing classes against + * @return A {@link DescribedPredicate} returning true, if and only if the tested {@link JavaClass} or one of + * its enclosing classes matches the given predicate. + */ + @PublicAPI(usage = ACCESS) + public static DescribedPredicate belongTo(DescribedPredicate predicate) { + return new BelongToPredicate(predicate); } /** @@ -2440,25 +2475,22 @@ public static DescribedPredicate containAnyStaticInitializersThat(Des private static final Function, Set> AS_SET = Optionals::asSet; - private static class BelongToAnyOfPredicate extends DescribedPredicate { - private final Class[] classes; + private static class BelongToPredicate extends DescribedPredicate { + private final DescribedPredicate predicate; - BelongToAnyOfPredicate(Class... classes) { - super("belong to any of " + formatNamesOf(classes)); - this.classes = classes; + BelongToPredicate(DescribedPredicate predicate) { + super("belong to " + predicate.getDescription()); + this.predicate = predicate; } @Override public boolean test(JavaClass input) { - return stream(classes).anyMatch(clazz -> belongsTo(input, clazz)); - } - - private boolean belongsTo(JavaClass input, Class clazz) { JavaClass toTest = input; - while (!toTest.isEquivalentTo(clazz) && toTest.getEnclosingClass().isPresent()) { + boolean result; + while (!(result = predicate.test(toTest)) && toTest.getEnclosingClass().isPresent()) { toTest = toTest.getEnclosingClass().get(); } - return toTest.isEquivalentTo(clazz); + return result; } } 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 c0b23ee8e4..cd63627dd6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java @@ -16,7 +16,6 @@ package com.tngtech.archunit.library; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; @@ -51,6 +50,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.DescribedPredicate.alwaysFalse; import static com.tngtech.archunit.base.DescribedPredicate.not; +import static com.tngtech.archunit.base.DescribedPredicate.or; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_ORIGIN_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_TARGET_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; @@ -82,7 +82,7 @@ private Architectures() { } /** - * Can be used to assert a typical layered architecture, e.g. with an UI layer, a business logic layer and + * Can be used to assert a typical layered architecture, e.g. with a UI layer, a business logic layer and * a persistence layer, where specific access rules should be adhered to, like UI may not access persistence * and each layer may only access lower layers, i.e. UI → business logic → persistence. *

@@ -668,10 +668,10 @@ public static final class OnionArchitecture implements ArchRule { private static final String ADAPTER_LAYER = "adapter"; private final Optional overriddenDescription; - private String[] domainModelPackageIdentifiers = new String[0]; - private String[] domainServicePackageIdentifiers = new String[0]; - private String[] applicationPackageIdentifiers = new String[0]; - private Map adapterPackageIdentifiers = new LinkedHashMap<>(); + private Optional> domainModelPredicate = Optional.empty(); + private Optional> domainServicePredicate = Optional.empty(); + private Optional> applicationPredicate = Optional.empty(); + private Map> adapterPredicates = new LinkedHashMap<>(); private boolean optionalLayers = false; private List ignoredDependencies = new ArrayList<>(); @@ -679,41 +679,62 @@ private OnionArchitecture() { overriddenDescription = Optional.empty(); } - private OnionArchitecture(String[] domainModelPackageIdentifiers, - String[] domainServicePackageIdentifiers, - String[] applicationPackageIdentifiers, - Map adapterPackageIdentifiers, + private OnionArchitecture( + Optional> domainModelPredicate, + Optional> domainServicePredicate, + Optional> applicationPredicate, + Map> adapterPredicates, List ignoredDependencies, Optional overriddenDescription) { - this.domainModelPackageIdentifiers = domainModelPackageIdentifiers; - this.domainServicePackageIdentifiers = domainServicePackageIdentifiers; - this.applicationPackageIdentifiers = applicationPackageIdentifiers; - this.adapterPackageIdentifiers = adapterPackageIdentifiers; + this.domainModelPredicate = domainModelPredicate; + this.domainServicePredicate = domainServicePredicate; + this.applicationPredicate = applicationPredicate; + this.adapterPredicates = adapterPredicates; this.ignoredDependencies = ignoredDependencies; this.overriddenDescription = overriddenDescription; } @PublicAPI(usage = ACCESS) public OnionArchitecture domainModels(String... packageIdentifiers) { - domainModelPackageIdentifiers = packageIdentifiers; + return domainModels(byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture domainModels(DescribedPredicate predicate) { + domainModelPredicate = Optional.of(predicate); return this; } @PublicAPI(usage = ACCESS) public OnionArchitecture domainServices(String... packageIdentifiers) { - domainServicePackageIdentifiers = packageIdentifiers; + return domainServices(byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture domainServices(DescribedPredicate predicate) { + domainServicePredicate = Optional.of(predicate); return this; } @PublicAPI(usage = ACCESS) public OnionArchitecture applicationServices(String... packageIdentifiers) { - applicationPackageIdentifiers = packageIdentifiers; + return applicationServices(byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture applicationServices(DescribedPredicate predicate) { + applicationPredicate = Optional.of(predicate); return this; } @PublicAPI(usage = ACCESS) public OnionArchitecture adapter(String name, String... packageIdentifiers) { - adapterPackageIdentifiers.put(name, packageIdentifiers); + return adapter(name, byPackagePredicate(packageIdentifiers)); + } + + @PublicAPI(usage = ACCESS) + public OnionArchitecture adapter(String name, DescribedPredicate predicate) { + adapterPredicates.put(name, predicate); return this; } @@ -743,18 +764,22 @@ public OnionArchitecture ignoreDependency(DescribedPredicate return this; } + private DescribedPredicate byPackagePredicate(String[] packageIdentifiers) { + return resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers)); + } + private LayeredArchitecture layeredArchitectureDelegate() { LayeredArchitecture layeredArchitectureDelegate = layeredArchitecture().consideringAllDependencies() - .layer(DOMAIN_MODEL_LAYER).definedBy(domainModelPackageIdentifiers) - .layer(DOMAIN_SERVICE_LAYER).definedBy(domainServicePackageIdentifiers) - .layer(APPLICATION_SERVICE_LAYER).definedBy(applicationPackageIdentifiers) - .layer(ADAPTER_LAYER).definedBy(concatenateAll(adapterPackageIdentifiers.values())) + .layer(DOMAIN_MODEL_LAYER).definedBy(domainModelPredicate.orElse(alwaysFalse())) + .layer(DOMAIN_SERVICE_LAYER).definedBy(domainServicePredicate.orElse(alwaysFalse())) + .layer(APPLICATION_SERVICE_LAYER).definedBy(applicationPredicate.orElse(alwaysFalse())) + .layer(ADAPTER_LAYER).definedBy(or(adapterPredicates.values())) .whereLayer(DOMAIN_MODEL_LAYER).mayOnlyBeAccessedByLayers(DOMAIN_SERVICE_LAYER, APPLICATION_SERVICE_LAYER, ADAPTER_LAYER) .whereLayer(DOMAIN_SERVICE_LAYER).mayOnlyBeAccessedByLayers(APPLICATION_SERVICE_LAYER, ADAPTER_LAYER) .whereLayer(APPLICATION_SERVICE_LAYER).mayOnlyBeAccessedByLayers(ADAPTER_LAYER) .withOptionalLayers(optionalLayers); - for (Map.Entry adapter : adapterPackageIdentifiers.entrySet()) { + for (Map.Entry> adapter : adapterPredicates.entrySet()) { String adapterLayer = getAdapterLayer(adapter.getKey()); layeredArchitectureDelegate = layeredArchitectureDelegate .layer(adapterLayer).definedBy(adapter.getValue()) @@ -766,10 +791,6 @@ private LayeredArchitecture layeredArchitectureDelegate() { return layeredArchitectureDelegate.as(getDescription()); } - private String[] concatenateAll(Collection arrays) { - return arrays.stream().flatMap(Arrays::stream).toArray(String[]::new); - } - private String getAdapterLayer(String name) { return String.format("%s %s", name, ADAPTER_LAYER); } @@ -795,8 +816,8 @@ public ArchRule allowEmptyShould(boolean allowEmptyShould) { @Override public OnionArchitecture as(String newDescription) { - return new OnionArchitecture(domainModelPackageIdentifiers, domainServicePackageIdentifiers, - applicationPackageIdentifiers, adapterPackageIdentifiers, ignoredDependencies, + return new OnionArchitecture(domainModelPredicate, domainServicePredicate, + applicationPredicate, adapterPredicates, ignoredDependencies, Optional.of(newDescription)); } @@ -812,21 +833,23 @@ public String getDescription() { } List lines = newArrayList("Onion architecture consisting of" + (optionalLayers ? " (optional)" : "")); - if (domainModelPackageIdentifiers.length > 0) { - lines.add(String.format("domain models ('%s')", Joiner.on("', '").join(domainModelPackageIdentifiers))); - } - if (domainServicePackageIdentifiers.length > 0) { - lines.add(String.format("domain services ('%s')", Joiner.on("', '").join(domainServicePackageIdentifiers))); - } - if (applicationPackageIdentifiers.length > 0) { - lines.add(String.format("application services ('%s')", Joiner.on("', '").join(applicationPackageIdentifiers))); - } - for (Map.Entry adapter : adapterPackageIdentifiers.entrySet()) { - lines.add(String.format("adapter '%s' ('%s')", adapter.getKey(), Joiner.on("', '").join(adapter.getValue()))); + domainModelPredicate.ifPresent(describedPredicate -> + lines.add(String.format("domain models (%s)", describedPredicate.getDescription()))); + domainServicePredicate.ifPresent(describedPredicate -> + lines.add(String.format("domain services (%s)", describedPredicate.getDescription()))); + applicationPredicate.ifPresent(describedPredicate -> + lines.add(String.format("application services (%s)", describedPredicate.getDescription()))); + for (Map.Entry> adapter : adapterPredicates.entrySet()) { + lines.add(String.format("adapter '%s' (%s)", adapter.getKey(), adapter.getValue().getDescription())); } return Joiner.on(lineSeparator()).join(lines); } + @Override + public String toString() { + return getDescription(); + } + private static class IgnoredDependency { private final DescribedPredicate origin; private final DescribedPredicate target; diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java index 8083f5ec68..4149c546f7 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java @@ -53,6 +53,7 @@ import org.junit.runner.RunWith; import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.base.DescribedPredicate.describe; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_ORIGIN_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_TARGET_CLASS; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_CODE_UNITS; @@ -64,6 +65,7 @@ import static com.tngtech.archunit.core.domain.JavaClass.Predicates.INTERFACES; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableFrom; 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.belongToAnyOf; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.containAnyCodeUnitsThat; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.containAnyConstructorsThat; @@ -1830,15 +1832,26 @@ public void predicate_equivalentTo() { .hasDescription("equivalent to " + Parent.class.getName()); } + @DataProvider + public static Object[][] data_predicate_belong_to() { + return testForEach( + belongToAnyOf(Object.class, ClassWithNamedAndAnonymousInnerClasses.class), + belongTo(describe( + String.format("any of [%s, %s]", Object.class.getName(), ClassWithNamedAndAnonymousInnerClasses.class.getName()), + javaClass -> javaClass.isEquivalentTo(Object.class) || javaClass.isEquivalentTo(ClassWithNamedAndAnonymousInnerClasses.class))) + ); + } + @Test - public void predicate_belong_to() { + @UseDataProvider + public void test_predicate_belong_to(DescribedPredicate belongToPredicate) { JavaClasses classes = new ClassFileImporter().importPackagesOf(getClass()); JavaClass outerAnonymous = getOnlyClassSettingField(classes, ClassWithNamedAndAnonymousInnerClasses.name_of_fieldIndicatingOuterAnonymousInnerClass); JavaClass nestedAnonymous = getOnlyClassSettingField(classes, ClassWithNamedAndAnonymousInnerClasses.name_of_fieldIndicatingNestedAnonymousInnerClass); - assertThat(belongToAnyOf(Object.class, ClassWithNamedAndAnonymousInnerClasses.class)) + assertThat(belongToPredicate) .hasDescription(String.format("belong to any of [%s, %s]", Object.class.getName(), ClassWithNamedAndAnonymousInnerClasses.class.getName())) .accepts(classes.get(ClassWithNamedAndAnonymousInnerClasses.class)) diff --git a/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java b/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java index ef5bcea795..0b3b4ed93e 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java @@ -15,12 +15,15 @@ import com.tngtech.archunit.library.testclasses.onionarchitecture.application.ApplicationLayerClass; import com.tngtech.archunit.library.testclasses.onionarchitecture.domain.model.DomainModelLayerClass; import com.tngtech.archunit.library.testclasses.onionarchitecture.domain.service.DomainServiceLayerClass; +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.alwaysTrue; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleNameContaining; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleNameStartingWith; import static com.tngtech.archunit.library.Architectures.onionArchitecture; @@ -29,6 +32,7 @@ import static com.tngtech.archunit.library.LayeredArchitectureTest.expectedAccessViolationPattern; import static com.tngtech.archunit.library.LayeredArchitectureTest.expectedEmptyLayerPattern; import static com.tngtech.archunit.library.LayeredArchitectureTest.expectedFieldTypePattern; +import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; import static java.beans.Introspector.decapitalize; import static java.lang.System.lineSeparator; import static java.util.stream.Collectors.toSet; @@ -37,16 +41,29 @@ @RunWith(DataProviderRunner.class) public class OnionArchitectureTest { - @Test - public void onion_architecture_description() { - OnionArchitecture architecture = onionArchitecture() - .domainModels("onionarchitecture.domain.model..") - .domainServices("onionarchitecture.domain.service..") - .applicationServices("onionarchitecture.application..") - .adapter("cli", "onionarchitecture.adapter.cli..") - .adapter("persistence", "onionarchitecture.adapter.persistence..") - .adapter("rest", "onionarchitecture.adapter.rest.command..", "onionarchitecture.adapter.rest.query.."); + @DataProvider + public static Object[][] data_onion_architecture_description() { + return testForEach( + onionArchitecture() + .domainModels("onionarchitecture.domain.model..") + .domainServices("onionarchitecture.domain.service..") + .applicationServices("onionarchitecture.application..") + .adapter("cli", "onionarchitecture.adapter.cli..") + .adapter("persistence", "onionarchitecture.adapter.persistence..") + .adapter("rest", "onionarchitecture.adapter.rest.command..", "onionarchitecture.adapter.rest.query.."), + onionArchitecture() + .domainModels(alwaysTrue().as("'onionarchitecture.domain.model..'")) + .domainServices(alwaysTrue().as("'onionarchitecture.domain.service..'")) + .applicationServices(alwaysTrue().as("'onionarchitecture.application..'")) + .adapter("cli", alwaysTrue().as("'onionarchitecture.adapter.cli..'")) + .adapter("persistence", alwaysTrue().as("'onionarchitecture.adapter.persistence..'")) + .adapter("rest", alwaysTrue().as("'onionarchitecture.adapter.rest.command..', 'onionarchitecture.adapter.rest.query..'")) + ); + } + @Test + @UseDataProvider + public void test_onion_architecture_description(OnionArchitecture architecture) { assertThat(architecture.getDescription()).isEqualTo( "Onion architecture consisting of" + lineSeparator() + "domain models ('onionarchitecture.domain.model..')" + lineSeparator() + @@ -94,9 +111,23 @@ public void onion_architecture_because_clause() { assertThat(architecture.getDescription()).isEqualTo("overridden, because some reason"); } + @DataProvider + public static Object[][] data_onion_architecture_gathers_all_violations() { + return testForEach( + getTestOnionArchitecture(), + onionArchitecture() + .domainModels(resideInAnyPackage(absolute("onionarchitecture.domain.model"))) + .domainServices(resideInAnyPackage(absolute("onionarchitecture.domain.service"))) + .applicationServices(resideInAnyPackage(absolute("onionarchitecture.application"))) + .adapter("cli", resideInAnyPackage(absolute("onionarchitecture.adapter.cli"))) + .adapter("persistence", resideInAnyPackage(absolute("onionarchitecture.adapter.persistence"))) + .adapter("rest", resideInAnyPackage(absolute("onionarchitecture.adapter.rest"))) + ); + } + @Test - public void onion_architecture_gathers_all_violations() { - OnionArchitecture architecture = getTestOnionArchitecture(); + @UseDataProvider + public void test_onion_architecture_gathers_all_violations(OnionArchitecture architecture) { JavaClasses classes = new ClassFileImporter().importPackages(absolute("onionarchitecture")); EvaluationResult result = architecture.evaluate(classes); @@ -164,7 +195,7 @@ public void onion_architecture_rejects_empty_layers_if_layers_are_explicitly_not assertFailureOnionArchitectureWithEmptyLayers(result); } - private OnionArchitecture getTestOnionArchitecture() { + private static OnionArchitecture getTestOnionArchitecture() { return onionArchitecture() .domainModels(absolute("onionarchitecture.domain.model")) .domainServices(absolute("onionarchitecture.domain.service"))