From d664bfcc887b1ab769f916be7b0c05b5e5dccba9 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 19 Jun 2022 16:55:37 +0700 Subject: [PATCH 1/4] add tests for `JavaClass.getAll{Fields/Methods/Constructors}(..)` While these methods are not rocket-science and the delegating supplier is tested transitively by other methods we should still have rudimentary checks that these methods as public API are not broken. Signed-off-by: Peter Gafert --- .../archunit/core/domain/JavaClassTest.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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 4d0ebe326b..8083f5ec68 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 @@ -210,14 +210,22 @@ public void finds_fields_and_methods() { assertThat(javaClass.reflect()).isEqualTo(ClassWithTwoFieldsAndTwoMethods.class); assertThat(javaClass.getFields()).hasSize(2); + Set allFields = excludeJavaLangObject(javaClass.getAllFields()); + assertThat(allFields).hasSize(3); assertThat(javaClass.getMethods()).hasSize(2); + Set allMethods = excludeJavaLangObject(javaClass.getAllMethods()); + assertThat(allMethods).hasSize(4); for (JavaField field : javaClass.getFields()) { assertThatType(field.getOwner()).isSameAs(javaClass); } + assertThatTypes(allFields.stream().map(JavaMember::getOwner).collect(toSet())) + .matchInAnyOrder(ClassWithTwoFieldsAndTwoMethods.class, SuperclassWithFieldAndMethod.class); for (JavaCodeUnit method : javaClass.getCodeUnits()) { assertThatType(method.getOwner()).isSameAs(javaClass); } + assertThatTypes(allMethods.stream().map(JavaMember::getOwner).collect(toSet())) + .matchInAnyOrder(ClassWithTwoFieldsAndTwoMethods.class, SuperclassWithFieldAndMethod.class, InterfaceWithMethod.class); } @Test @@ -228,6 +236,11 @@ public void finds_constructors() { assertThat(javaClass.getConstructors()).is(containing(codeUnitWithSignature(CONSTRUCTOR_NAME))); assertThat(javaClass.getConstructors()).is(containing(codeUnitWithSignature(CONSTRUCTOR_NAME, String.class))); assertThat(javaClass.getConstructors()).is(containing(codeUnitWithSignature(CONSTRUCTOR_NAME, int.class, Object[].class))); + + Set allConstructors = excludeJavaLangObject(javaClass.getAllConstructors()); + assertThat(allConstructors).as("all constructors").hasSize(5); + assertThatTypes(allConstructors.stream().map(JavaConstructor::getOwner).collect(toSet())) + .matchInAnyOrder(ClassWithSeveralConstructorsFieldsAndMethods.class, ParentOfClassWithSeveralConstructorsFieldsAndMethods.class); } @Test @@ -1955,6 +1968,10 @@ class Mismatch { .rejects(classes.get(Mismatch.class)); } + private Set excludeJavaLangObject(Set members) { + return members.stream().filter(it -> !it.getOwner().isEquivalentTo(Object.class)).collect(toSet()); + } + private JavaClass getOnlyClassSettingField(JavaClasses classes, final String fieldName) { return getOnlyElement(classes.that(new DescribedPredicate("") { @Override @@ -2259,7 +2276,16 @@ abstract static class Parent { } @SuppressWarnings("unused") - static class ClassWithSeveralConstructorsFieldsAndMethods { + static class ParentOfClassWithSeveralConstructorsFieldsAndMethods { + ParentOfClassWithSeveralConstructorsFieldsAndMethods() { + } + + ParentOfClassWithSeveralConstructorsFieldsAndMethods(Object anyParam) { + } + } + + @SuppressWarnings("unused") + static class ClassWithSeveralConstructorsFieldsAndMethods extends ParentOfClassWithSeveralConstructorsFieldsAndMethods { String stringField; private int intField; From 77d9406080fbf4eecfd0f4214d1c20ed0df3962e Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 19 Jun 2022 17:17:13 +0700 Subject: [PATCH 2/4] move `PackageMatcher(s)` to package `core.domain` This class does not really match the generic nature of classes in the `base` package, since it is clearly related to ArchUnit's domain. When some predicates were moved from `lang.conditions` to `domain.core` `PackageMatcher` was moved to `base`, probably out of the idea that then "every place within ArchUnit can utilize it". But in turn `PackageMatcher` can then not use any utilities from `core`, which makes no sense. Or all such utilities will start to creep into `base` as well. In the end, `core.domain` is the next higher layer on top of `base` in the ArchUnit layered architecture. And within `base` there should be no need ever to match packages, etc., since that is already ArchUnit domain specific logic. Since we are about to release 1.0 and can thus break the public API this is a good point in time to move this class back into a more reasonable layer. Signed-off-by: Peter Gafert --- .../archunit/exampletest/junit4/ControllerRulesTest.java | 2 +- .../exampletest/junit4/PlantUmlArchitectureTest.java | 2 +- .../archunit/exampletest/junit5/ControllerRulesTest.java | 2 +- .../exampletest/junit5/PlantUmlArchitectureTest.java | 2 +- .../tngtech/archunit/exampletest/ControllerRulesTest.java | 2 +- .../archunit/exampletest/PlantUmlArchitectureTest.java | 2 +- .../java/com/tngtech/archunit/core/domain/JavaClass.java | 1 - .../archunit/{base => core/domain}/PackageMatcher.java | 2 +- .../archunit/{base => core/domain}/PackageMatchers.java | 3 ++- .../tngtech/archunit/lang/conditions/ArchConditions.java | 4 ++-- .../lang/conditions/JavaAccessPackagePredicate.java | 2 +- .../archunit/lang/syntax/elements/ClassesShould.java | 2 +- .../tngtech/archunit/lang/syntax/elements/ClassesThat.java | 2 +- .../lang/syntax/elements/OnlyBeAccessedSpecification.java | 2 +- .../java/com/tngtech/archunit/library/Architectures.java | 2 +- .../com/tngtech/archunit/library/dependencies/Slices.java | 4 ++-- .../library/plantuml/JavaClassDiagramAssociation.java | 2 +- .../archunit/library/plantuml/PlantUmlArchCondition.java | 4 ++-- .../archunit/{base => core/domain}/PackageMatcherTest.java | 6 +++--- .../archunit/{base => core/domain}/PackageMatchersTest.java | 4 ++-- 20 files changed, 26 insertions(+), 26 deletions(-) rename archunit/src/main/java/com/tngtech/archunit/{base => core/domain}/PackageMatcher.java (99%) rename archunit/src/main/java/com/tngtech/archunit/{base => core/domain}/PackageMatchers.java (95%) rename archunit/src/test/java/com/tngtech/archunit/{base => core/domain}/PackageMatcherTest.java (97%) rename archunit/src/test/java/com/tngtech/archunit/{base => core/domain}/PackageMatchersTest.java (93%) diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java index e9b9e6f9a8..9ba5123ff0 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ControllerRulesTest.java @@ -1,9 +1,9 @@ package com.tngtech.archunit.exampletest.junit4; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaMember; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.example.layers.security.Secured; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; 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 03662eef38..a0240f786e 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 @@ -2,7 +2,7 @@ import java.net.URL; -import com.tngtech.archunit.base.PackageMatchers; +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; diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java index 0164e9de67..ed69fd4ee0 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ControllerRulesTest.java @@ -1,9 +1,9 @@ package com.tngtech.archunit.exampletest.junit5; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaMember; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.example.layers.security.Secured; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTag; 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 85d49fdbf4..9b1abe0ef4 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 @@ -2,7 +2,7 @@ import java.net.URL; -import com.tngtech.archunit.base.PackageMatchers; +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; diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java index 6a0ec56301..f8ef44aee6 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ControllerRulesTest.java @@ -1,10 +1,10 @@ package com.tngtech.archunit.exampletest; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaMember; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.example.layers.security.Secured; import org.junit.Test; 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 38836e99b3..516e0084b7 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 @@ -2,8 +2,8 @@ import java.net.URL; -import com.tngtech.archunit.base.PackageMatchers; 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; 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 c20b174d4e..5de8176bb8 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 @@ -34,7 +34,6 @@ import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.MayResolveTypesViaReflection; import com.tngtech.archunit.base.Optionals; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.base.ResolvesTypesViaReflection; import com.tngtech.archunit.base.Suppliers; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; diff --git a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatcher.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java similarity index 99% rename from archunit/src/main/java/com/tngtech/archunit/base/PackageMatcher.java rename to archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java index 7d539264c2..059e7b1d0c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatcher.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import java.util.List; import java.util.Optional; diff --git a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatchers.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java similarity index 95% rename from archunit/src/main/java/com/tngtech/archunit/base/PackageMatchers.java rename to archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java index eccab551a3..b2ae062024 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/PackageMatchers.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import java.util.Collection; import java.util.Set; @@ -21,6 +21,7 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.DescribedPredicate; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java index 2ac14dd2e3..fb3b5091d1 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java @@ -28,8 +28,6 @@ import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.HasDescription; -import com.tngtech.archunit.base.PackageMatcher; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.AccessTarget; import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitCallTarget; import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; @@ -50,6 +48,8 @@ import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaMethodCall; import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.core.domain.PackageMatcher; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.core.domain.properties.HasAnnotations; import com.tngtech.archunit.core.domain.properties.HasModifiers; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java index 33bf462d5a..7c1b0e56f5 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java @@ -19,8 +19,8 @@ import com.google.common.base.Joiner; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.JavaAccess; +import com.tngtech.archunit.core.domain.PackageMatchers; class JavaAccessPackagePredicate extends DescribedPredicate> { private final Function, String> getPackageName; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java index 853699cd6c..8c4cbc5171 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesShould.java @@ -19,7 +19,6 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.AccessTarget; import com.tngtech.archunit.core.domain.JavaAccess; import com.tngtech.archunit.core.domain.JavaAnnotation; @@ -34,6 +33,7 @@ import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaMethodCall; import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.core.domain.PackageMatcher; import com.tngtech.archunit.core.domain.properties.HasName.Predicates; import com.tngtech.archunit.lang.conditions.ArchConditions; import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java index 0f5e005874..c705a9472c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/ClassesThat.java @@ -19,7 +19,6 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaCodeUnit; @@ -29,6 +28,7 @@ import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaModifier; import com.tngtech.archunit.core.domain.JavaStaticInitializer; +import com.tngtech.archunit.core.domain.PackageMatcher; import com.tngtech.archunit.core.domain.properties.HasName.Predicates; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java index b14d4d7d99..ba7fa97bfd 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/syntax/elements/OnlyBeAccessedSpecification.java @@ -17,8 +17,8 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.PackageMatcher; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; 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 a582a13d3a..a410156507 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java @@ -31,10 +31,10 @@ import com.google.common.base.Joiner; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; 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.PackageMatcher; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java index c0ffaba466..c191547999 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slices.java @@ -30,10 +30,10 @@ import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedIterable; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; 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.PackageMatcher; import com.tngtech.archunit.core.domain.properties.CanOverrideDescription; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ClassesTransformer; @@ -41,8 +41,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; -import static com.tngtech.archunit.base.PackageMatcher.TO_GROUPS; import static com.tngtech.archunit.core.domain.Dependency.toTargetClasses; +import static com.tngtech.archunit.core.domain.PackageMatcher.TO_GROUPS; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java index 2bbc27bbe5..f69813878d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/JavaClassDiagramAssociation.java @@ -21,8 +21,8 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; -import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.PackageMatcher; import static com.google.common.collect.Iterables.getOnlyElement; import static java.util.stream.Collectors.toCollection; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java index d145c59fc9..e649a3687c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java @@ -28,10 +28,10 @@ import com.google.common.collect.Sets; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.base.PackageMatcher; -import com.tngtech.archunit.base.PackageMatchers; import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.PackageMatcher; +import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ConditionEvents; diff --git a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatcherTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatcherTest.java similarity index 97% rename from archunit/src/test/java/com/tngtech/archunit/base/PackageMatcherTest.java rename to archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatcherTest.java index 76958715f1..d64d2cc86a 100644 --- a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatcherTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatcherTest.java @@ -1,8 +1,8 @@ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import java.util.Optional; -import com.tngtech.archunit.base.PackageMatcher.Result; +import com.tngtech.archunit.core.domain.PackageMatcher.Result; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import org.junit.Rule; @@ -10,7 +10,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; -import static com.tngtech.archunit.base.PackageMatcher.TO_GROUPS; +import static com.tngtech.archunit.core.domain.PackageMatcher.TO_GROUPS; import static com.tngtech.archunit.testutil.Assertions.assertThat; @RunWith(DataProviderRunner.class) diff --git a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatchersTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatchersTest.java similarity index 93% rename from archunit/src/test/java/com/tngtech/archunit/base/PackageMatchersTest.java rename to archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatchersTest.java index 1c0fce82f5..c6662e1874 100644 --- a/archunit/src/test/java/com/tngtech/archunit/base/PackageMatchersTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/PackageMatchersTest.java @@ -1,4 +1,4 @@ -package com.tngtech.archunit.base; +package com.tngtech.archunit.core.domain; import org.junit.Test; @@ -20,4 +20,4 @@ public void description() { assertThat(PackageMatchers.of("..foo..", "..bar..")) .hasDescription("matches any of ['..foo..', '..bar..']"); } -} \ No newline at end of file +} From 163b59bc9f8282e212685fcb907ba911da43a47e Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 19 Jun 2022 16:08:33 +0700 Subject: [PATCH 3/4] introduce method `Formatters.joinSingleQuoted(..)` It obviously is a reoccurring task to join some strings together on comma and single quote them as part of rule or violation descriptions. We can offer this through the public API so users can also benefit from it for their custom rules. Signed-off-by: Peter Gafert --- .../archunit/core/domain/Formatters.java | 39 ++++++++++++++++--- .../archunit/core/domain/JavaClass.java | 6 +-- .../archunit/core/domain/PackageMatchers.java | 4 +- .../lang/conditions/ArchConditions.java | 7 ++-- .../JavaAccessPackagePredicate.java | 5 ++- .../archunit/library/Architectures.java | 12 +++--- .../plantuml/PlantUmlArchCondition.java | 4 +- .../archunit/core/domain/FormattersTest.java | 21 ++++++++++ .../tngtech/archunit/lang/ArchRuleTest.java | 3 +- .../syntax/elements/ClassesShouldTest.java | 15 ++++--- .../syntax/elements/GivenClassShouldTest.java | 26 ++++++------- 11 files changed, 95 insertions(+), 47 deletions(-) diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java index 9949b3aeb6..2971d619c0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java @@ -16,6 +16,8 @@ package com.tngtech.archunit.core.domain; import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; @@ -24,6 +26,8 @@ import static com.google.common.base.Strings.repeat; import static com.google.common.collect.ImmutableList.copyOf; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; public final class Formatters { @@ -97,18 +101,19 @@ public static List formatNamesOf(Iterable> paramTypes return result.build(); } - // Excluding the '$' character might be incorrect, but since '$' is a valid character of a class name - // and also the delimiter within the fully qualified name between an inner class and its enclosing class, - // there is no clean way to derive the simple name from just a fully qualified class name without - // further information - // Luckily for imported classes we can read this information from the bytecode /** * @param name A possibly fully qualified class name * @return A best guess of the simple name, i.e. prefixes like 'a.b.c.' cut off, 'Some$' of 'Some$Inner' as well. - * Returns an empty String, if the name belongs to an anonymous class (e.g. some.Type$1). + * Returns an empty String, if the name belongs to an anonymous class (e.g. some.Type$1). */ @PublicAPI(usage = ACCESS) public static String ensureSimpleName(String name) { + // Excluding the '$' character might be incorrect, but since '$' is a valid character of a class name + // and also the delimiter within the fully qualified name between an inner class and its enclosing class, + // there is no clean way to derive the simple name from just a fully qualified class name without + // further information + // Luckily for imported classes we can read this information from the bytecode + int lastIndexOfDot = name.lastIndexOf('.'); String partAfterDot = lastIndexOfDot >= 0 ? name.substring(lastIndexOfDot + 1) : name; @@ -151,4 +156,26 @@ public static String ensureCanonicalArrayTypeName(String typeName) { private static boolean isNoArrayClassName(String typeName) { return !typeName.startsWith("["); } + + /** + * @see #joinSingleQuoted(Iterable) + */ + @PublicAPI(usage = ACCESS) + public static String joinSingleQuoted(String... strings) { + return joinSingleQuoted(stream(strings)); + } + + /** + * @param strings Any number of strings + * @return The strings concatenated on ',' and each wrapped in single quotes. E.g. {@code ["a", "b", "c"] -> "'a', 'b', 'c'"} + */ + @PublicAPI(usage = ACCESS) + public static String joinSingleQuoted(Iterable strings) { + return joinSingleQuoted(StreamSupport.stream(strings.spliterator(), false)); + } + + private static String joinSingleQuoted(Stream strings) { + String joinedElements = strings.collect(joining("', '")); + return joinedElements.isEmpty() ? joinedElements : "'" + joinedElements + "'"; + } } 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 5de8176bb8..6c2e53e8f1 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 @@ -25,7 +25,6 @@ import java.util.function.Function; import java.util.function.Supplier; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; @@ -52,6 +51,7 @@ 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.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_CODE_UNITS; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_CONSTRUCTORS; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_FIELDS; @@ -2325,7 +2325,7 @@ public static DescribedPredicate resideInAPackage(final String packag @PublicAPI(usage = ACCESS) public static DescribedPredicate resideInAnyPackage(final String... packageIdentifiers) { return resideInAnyPackage(packageIdentifiers, - String.format("reside in any package ['%s']", Joiner.on("', '").join(packageIdentifiers))); + String.format("reside in any package [%s]", joinSingleQuoted(packageIdentifiers))); } private static DescribedPredicate resideInAnyPackage(final String[] packageIdentifiers, final String description) { @@ -2342,7 +2342,7 @@ public static DescribedPredicate resideOutsideOfPackage(String packag @PublicAPI(usage = ACCESS) public static DescribedPredicate resideOutsideOfPackages(String... packageIdentifiers) { return not(JavaClass.Predicates.resideInAnyPackage(packageIdentifiers)) - .as("reside outside of packages ['%s']", Joiner.on("', '").join(packageIdentifiers)); + .as("reside outside of packages [%s]", joinSingleQuoted(packageIdentifiers)); } /** diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java index b2ae062024..d1a3d8c8c3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java @@ -18,19 +18,19 @@ import java.util.Collection; import java.util.Set; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; @PublicAPI(usage = ACCESS) public final class PackageMatchers extends DescribedPredicate { private final Set packageMatchers; private PackageMatchers(Set packageIdentifiers) { - super("matches any of ['%s']", Joiner.on("', '").join(packageIdentifiers)); + super("matches any of [%s]", joinSingleQuoted(packageIdentifiers)); ImmutableSet.Builder matchers = ImmutableSet.builder(); for (String identifier : packageIdentifiers) { matchers.add(PackageMatcher.of(identifier)); diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java index fb3b5091d1..1e9233b8c8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/ArchConditions.java @@ -73,6 +73,7 @@ import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependencyTarget; import static com.tngtech.archunit.core.domain.Formatters.ensureSimpleName; import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_ACCESSES_FROM_SELF; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_ACCESSES_TO_SELF; import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_CODE_UNIT_CALLS_FROM_SELF; @@ -362,8 +363,7 @@ public static ArchCondition onlyBeAccessedByAnyPackage(String... pack */ @PublicAPI(usage = ACCESS) public static ArchCondition onlyHaveDependentsInAnyPackage(String... packageIdentifiers) { - String description = String.format("only have dependents in any package ['%s']", - Joiner.on("', '").join(packageIdentifiers)); + String description = String.format("only have dependents in any package [%s]", joinSingleQuoted(packageIdentifiers)); return onlyHaveDependentsWhere(dependencyOrigin(GET_PACKAGE_NAME.is(PackageMatchers.of(packageIdentifiers)))) .as(description); } @@ -398,8 +398,7 @@ public static ArchCondition onlyHaveDependentsWhere(DescribedPredicat */ @PublicAPI(usage = ACCESS) public static AllDependenciesCondition onlyHaveDependenciesInAnyPackage(String... packageIdentifiers) { - String description = String.format("only have dependencies in any package ['%s']", - Joiner.on("', '").join(packageIdentifiers)); + String description = String.format("only have dependencies in any package [%s]", joinSingleQuoted(packageIdentifiers)); return onlyHaveDependenciesWhere(dependencyTarget(GET_PACKAGE_NAME.is(PackageMatchers.of(packageIdentifiers)))) .as(description); } diff --git a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java index 7c1b0e56f5..bd239b2c46 100644 --- a/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java +++ b/archunit/src/main/java/com/tngtech/archunit/lang/conditions/JavaAccessPackagePredicate.java @@ -17,17 +17,18 @@ import java.util.function.Function; -import com.google.common.base.Joiner; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaAccess; import com.tngtech.archunit.core.domain.PackageMatchers; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; + class JavaAccessPackagePredicate extends DescribedPredicate> { private final Function, String> getPackageName; private final PackageMatchers packageMatchers; private JavaAccessPackagePredicate(String[] packageIdentifiers, Function, String> getPackageName) { - super(String.format("any package ['%s']", Joiner.on("', '").join(packageIdentifiers))); + super(String.format("any package [%s]", joinSingleQuoted(packageIdentifiers))); this.getPackageName = getPackageName; packageMatchers = PackageMatchers.of(packageIdentifiers); } 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 a410156507..c0b23ee8e4 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java @@ -56,6 +56,7 @@ import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependencyOrigin; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependencyTarget; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; 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.resideOutsideOfPackages; @@ -457,8 +458,7 @@ public LayeredArchitecture definedBy(DescribedPredicate predi */ @PublicAPI(usage = ACCESS) public LayeredArchitecture definedBy(String... packageIdentifiers) { - String description = String.format("'%s'", Joiner.on("', '").join(packageIdentifiers)); - return definedBy(resideInAnyPackage(packageIdentifiers).as(description)); + return definedBy(resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers))); } boolean isOptional() { @@ -506,7 +506,7 @@ public LayeredArchitecture mayNotBeAccessedByAnyLayer() { */ @PublicAPI(usage = ACCESS) public LayeredArchitecture mayOnlyBeAccessedByLayers(String... layerNames) { - return restrictLayers(LayerDependencyConstraint.ORIGIN, layerNames, "may only be accessed by layers ['%s']"); + return restrictLayers(LayerDependencyConstraint.ORIGIN, layerNames, "may only be accessed by layers [%s]"); } /** @@ -525,7 +525,7 @@ public LayeredArchitecture mayNotAccessAnyLayer() { */ @PublicAPI(usage = ACCESS) public LayeredArchitecture mayOnlyAccessLayers(String... layerNames) { - return restrictLayers(LayerDependencyConstraint.TARGET, layerNames, "may only access layers ['%s']"); + return restrictLayers(LayerDependencyConstraint.TARGET, layerNames, "may only access layers [%s]"); } private LayeredArchitecture denyLayerAccess(LayerDependencyConstraint constraint, String description) { @@ -540,7 +540,7 @@ private LayeredArchitecture restrictLayers(LayerDependencyConstraint constraint, checkLayerNamesExist(layerNames); allowedLayers.addAll(asList(layerNames)); this.constraint = constraint; - descriptionSuffix = String.format(descriptionTemplate, Joiner.on("', '").join(layerNames)); + descriptionSuffix = String.format(descriptionTemplate, joinSingleQuoted(layerNames)); return LayeredArchitecture.this.addDependencySpecification(this); } @@ -635,7 +635,7 @@ private DependencySettings setToConsideringAllDependencies() { private DependencySettings setToConsideringOnlyDependenciesInAnyPackage(String[] packageIdentifiers) { DescribedPredicate outsideOfRelevantPackage = resideOutsideOfPackages(packageIdentifiers); return new DependencySettings( - String.format("considering only dependencies in any package ['%s']", Joiner.on("', '").join(packageIdentifiers)), + String.format("considering only dependencies in any package [%s]", joinSingleQuoted(packageIdentifiers)), (__, predicate) -> predicate.or(originOrTargetIs(outsideOfRelevantPackage)) ); } diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java index e649a3687c..de83eeadd4 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/plantuml/PlantUmlArchCondition.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.function.Function; -import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.google.common.collect.Sets; import com.tngtech.archunit.PublicAPI; @@ -39,6 +38,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; 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.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.lang.conditions.ArchConditions.onlyHaveDependenciesInAnyPackage; import static java.util.Collections.singleton; @@ -281,7 +281,7 @@ private static class NotContainedInPackagesPredicate extends DescribedPredicate< private final List packageIdentifiers; NotContainedInPackagesPredicate(List packageIdentifiers) { - super(" while ignoring dependencies outside of packages ['%s']", Joiner.on("', '").join(packageIdentifiers)); + super(" while ignoring dependencies outside of packages [%s]", joinSingleQuoted(packageIdentifiers)); this.packageIdentifiers = packageIdentifiers; } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java index f83c8b512a..52d8ba5f4f 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/FormattersTest.java @@ -21,10 +21,13 @@ import static com.google.common.collect.ImmutableList.of; import static com.google.common.collect.Sets.union; import static com.google.common.primitives.Primitives.allPrimitiveTypes; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.testutil.Assertions.assertThat; import static com.tngtech.java.junit.dataprovider.DataProviders.$; import static com.tngtech.java.junit.dataprovider.DataProviders.$$; +import static java.util.Arrays.stream; import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toList; @RunWith(DataProviderRunner.class) public class FormattersTest { @@ -117,4 +120,22 @@ private static List> generateCanonicalNameTestCases(Iterable containingOnlyLinesWith(final String[] messages) return new TypeSafeMatcher() { @Override public void describeTo(Description description) { - description.appendText(String.format("Only the error messages '%s'", Joiner.on("', '").join(messages))); + description.appendText(String.format("Only the error messages %s", joinSingleQuoted(messages))); } @Override diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java index 300bf24fc7..f775099a70 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ClassesShouldTest.java @@ -45,6 +45,7 @@ import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type; import static com.tngtech.archunit.core.domain.JavaClassTest.expectInvalidSyntaxUsageForClassInsteadOfInterface; import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; @@ -425,8 +426,7 @@ public void resideInAnyPackage(ArchRule rule, String... packageIdentifiers) { ArchRule.class, ArchConfiguration.class, GivenObjects.class)); assertThat(singleLineFailureReportOf(result)) - .contains(String.format("classes should reside in any package ['%s']", - Joiner.on("', '").join(packageIdentifiers))) + .contains(String.format("classes should reside in any package [%s]", joinSingleQuoted(packageIdentifiers))) .containsPattern(doesntResideInAnyPackagePatternFor(GivenObjects.class, packageIdentifiers)) .doesNotContain(String.format("%s", ArchRule.class.getSimpleName())) .doesNotContain(String.format("%s", ArchConfiguration.class.getSimpleName())); @@ -483,8 +483,7 @@ public void resideOutsideOfPackages(ArchRule rule, String... packageIdentifiers) ArchRule.class, ArchCondition.class, ArchConfiguration.class, GivenObjects.class)); assertThat(singleLineFailureReportOf(result)) - .contains(String.format("classes should reside outside of packages ['%s']", - Joiner.on("', '").join(packageIdentifiers))) + .contains(String.format("classes should reside outside of packages [%s]", joinSingleQuoted(packageIdentifiers))) .containsPattern(doesntResideOutsideOfPackagesPatternFor(ArchRule.class, packageIdentifiers)) .containsPattern(doesntResideOutsideOfPackagesPatternFor(ArchCondition.class, packageIdentifiers)) .doesNotContain(String.format("%s", GivenObjects.class.getSimpleName())); @@ -1835,13 +1834,13 @@ private String doesntResideOutsideOfPackagePatternFor(Class clazz, String pac @SuppressWarnings("SameParameterValue") private String doesntResideInAnyPackagePatternFor(Class clazz, String[] packageIdentifiers) { - return String.format("Class <%s> does not reside in any package \\['%s'\\] in %s", - quote(clazz.getName()), quote(Joiner.on("', '").join(packageIdentifiers)), locationPattern(clazz)); + return String.format("Class <%s> does not reside in any package \\[%s\\] in %s", + quote(clazz.getName()), quote(joinSingleQuoted(packageIdentifiers)), locationPattern(clazz)); } private String doesntResideOutsideOfPackagesPatternFor(Class clazz, String[] packageIdentifiers) { - return String.format("Class <%s> does not reside outside of packages \\['%s'\\] in %s", - quote(clazz.getName()), quote(Joiner.on("', '").join(packageIdentifiers)), locationPattern(clazz)); + return String.format("Class <%s> does not reside outside of packages \\[%s\\] in %s", + quote(clazz.getName()), quote(joinSingleQuoted(packageIdentifiers)), locationPattern(clazz)); } private static DescribedPredicate> callTargetIs(Class type) { diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java index e87c630425..91fc2308c6 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/GivenClassShouldTest.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.regex.Pattern; -import com.google.common.base.Joiner; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaModifier; import com.tngtech.archunit.core.domain.properties.CanBeAnnotatedTest.RuntimeRetentionAnnotation; @@ -22,6 +21,7 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import static com.tngtech.archunit.core.domain.Formatters.joinSingleQuoted; import static com.tngtech.archunit.core.domain.JavaModifier.PUBLIC; import static com.tngtech.archunit.core.domain.TestUtils.importClasses; import static com.tngtech.archunit.lang.conditions.ArchConditions.haveOnlyPrivateConstructors; @@ -520,15 +520,15 @@ public void theClass_should_resideInAnyPackage(ArchRule satisfiedRule, ArchRule String[] packageIdentifiers = {firstPackage, secondPackage}; assertThatRules(satisfiedRule, unsatisfiedRule, SomeClass.class, Object.class) - .haveSuccessfulRuleText("the class %s should reside in any package ['%s']", + .haveSuccessfulRuleText("the class %s should reside in any package [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .haveFailingRuleText("the class %s should reside outside of packages ['%s']", + joinSingleQuoted(packageIdentifiers)) + .haveFailingRuleText("the class %s should reside outside of packages [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .containFailureDetail(String.format("Class <%s> does not reside outside of packages \\['%s'\\] in %s", + joinSingleQuoted(packageIdentifiers)) + .containFailureDetail(String.format("Class <%s> does not reside outside of packages \\[%s\\] in %s", quote(SomeClass.class.getName()), - quote(Joiner.on("', '").join(packageIdentifiers)), + quote(joinSingleQuoted(packageIdentifiers)), locationPattern(SomeClass.class))) .doNotContainFailureDetail(quote(Object.class.getName())); } @@ -558,15 +558,15 @@ public void noClass_should_resideInAnyPackage(ArchRule satisfiedRule, ArchRule u String[] packageIdentifiers = {firstPackage, secondPackage}; assertThatRules(satisfiedRule, unsatisfiedRule, SomeClass.class, Object.class) - .haveSuccessfulRuleText("no class %s should reside outside of packages ['%s']", + .haveSuccessfulRuleText("no class %s should reside outside of packages [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .haveFailingRuleText("no class %s should reside in any package ['%s']", + joinSingleQuoted(packageIdentifiers)) + .haveFailingRuleText("no class %s should reside in any package [%s]", SomeClass.class.getName(), - Joiner.on("', '").join(packageIdentifiers)) - .containFailureDetail(String.format("Class <%s> does reside in any package \\['%s'\\] in %s", + joinSingleQuoted(packageIdentifiers)) + .containFailureDetail(String.format("Class <%s> does reside in any package \\[%s\\] in %s", quote(SomeClass.class.getName()), - quote(Joiner.on("', '").join(packageIdentifiers)), + quote(joinSingleQuoted(packageIdentifiers)), locationPattern(SomeClass.class))) .doNotContainFailureDetail(quote(Object.class.getName())); } From 6046075f2849022fcb39bacb5ef70e83c7227009 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sat, 18 Jun 2022 18:45:27 +0700 Subject: [PATCH 4/4] extend Onion Architecture to allow defining components by predicate Some users use different conventions, like class naming or annotations, to define the components of their Onion Architecture. To support such use cases we extend `OnionArchitecture` similar to `LayeredArchitecture` to also allow defining components via any arbitrary `DescribedPredicate`. While extending the example I noticed that it might be a good addition to have a more generic `JavaClass.Predicates.belongTo(DescribedPredicate)` method additionally to `belongToAnyOf(Class...)`. At least writing the example would otherwise have been tedious, since assigning nested and anonymous classes to the annotation of the outer class would always be necessary for this use case. Signed-off-by: Peter Gafert --- .../junit4/OnionArchitectureTest.java | 44 +++++++- .../junit5/OnionArchitectureTest.java | 44 +++++++- .../annotations/Adapter.java | 5 + .../annotations/Application.java | 4 + .../annotations/DomainModel.java | 4 + .../annotations/DomainService.java | 4 + .../onion/AdministrationPort.java | 8 ++ .../onion/ShoppingApplication.java | 19 ++++ .../administration/AdministrationCLI.java | 22 ++++ .../onion/order/OrderItem.java | 26 +++++ .../onion/order/OrderQuantity.java | 21 ++++ .../onion/order/PaymentMethod.java | 7 ++ .../onion/product/Product.java | 27 +++++ .../onion/product/ProductId.java | 23 ++++ .../onion/product/ProductJpaRepository.java | 17 +++ .../onion/product/ProductName.java | 21 ++++ .../onion/product/ProductRepository.java | 11 ++ .../onion/shopping/ShoppingCart.java | 32 ++++++ .../onion/shopping/ShoppingCartId.java | 22 ++++ .../shopping/ShoppingCartJpaRepository.java | 18 +++ .../shopping/ShoppingCartRepository.java | 11 ++ .../onion/shopping/ShoppingController.java | 22 ++++ .../onion/shopping/ShoppingService.java | 27 +++++ .../exampletest/OnionArchitectureTest.java | 46 +++++++- .../integration/ExamplesIntegrationTest.java | 2 + ...OnionArchitectureByAnnotationFailures.java | 77 +++++++++++++ .../archunit/core/domain/JavaClass.java | 87 ++++++++++++--- .../archunit/library/Architectures.java | 103 +++++++++++------- .../archunit/core/domain/JavaClassTest.java | 17 ++- .../library/OnionArchitectureTest.java | 55 ++++++++-- 30 files changed, 754 insertions(+), 72 deletions(-) create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Adapter.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/Application.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainModel.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/annotations/DomainService.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/AdministrationPort.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/ShoppingApplication.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/administration/AdministrationCLI.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderItem.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/OrderQuantity.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/order/PaymentMethod.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/Product.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductId.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductJpaRepository.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductName.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/product/ProductRepository.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCart.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartId.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartJpaRepository.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingCartRepository.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingController.java create mode 100644 archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/onionarchitecture_by_annotations/onion/shopping/ShoppingService.java create mode 100644 archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java 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..cf1dffc137 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 @@ -2355,15 +2355,74 @@ 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. + * where "belong" means that this {@link JavaClass} is equivalent to + *
    + *
  • any of the passed {@link Class classes}
  • + *
  • any nested/inner/anonymous class of one of the passed {@link Class classes}
  • + *
* - * @param classes The {@link Class classes} to check the {@link JavaClass} against + * For example {@code belongToAnyOf(Outer.class)} would apply to the following cases + *

+         * class Outer {
+         *     // Inner would match belongToAnyOf(Outer.class) since it is an inner class of Outer
+         *     class Inner {}
+         *
+         *     void call() {
+         *         // this anonymous class would also match belongToAnyOf(Outer.class) since it is declared within Outer
+         *         new Serializable() {}
+         *     }
+         * }
+         *
+         * // this class would not match, since it is neither Outer itself nor nested within the class body of Outer
+         * class Other {}
+         * 
+ * + * @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 "belong" means that this {@link JavaClass} is + *
    + *
  • directly matching the given predicate
  • + *
  • a nested/inner/anonymous class of another {@link JavaClass} matching the predicate
  • + *
+ * + * 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 +2499,23 @@ 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 matches = predicate.test(toTest); + while (!matches && toTest.getEnclosingClass().isPresent()) { toTest = toTest.getEnclosingClass().get(); + matches = predicate.test(toTest); } - return toTest.isEquivalentTo(clazz); + return matches; } } 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"))