From e2c9b223ad58aefcf7c7ba64fb57fec0065a309b Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 10 Mar 2019 19:35:07 +0100 Subject: [PATCH] Makes Slices more customizable. Background: In legacy applications the package structure might not be as neat as myapp.(*) where * catches all reasonable domain slices. Instead it might be necessary to really tweak which classes from which locations form a 'slice'. The new approach allows to freely define a mapping JavaClass -> SliceIdentifier to group arbitrary classes together into one slice (defined by the common SliceIdentifier). Signed-off-by: Peter Gafert --- .../junit4/CyclicDependencyRulesTest.java | 28 +++ .../junit5/CyclicDependencyRulesTest.java | 29 +++ .../CyclicDependencyRulesTest.java | 31 +++ .../integration/ExamplesIntegrationTest.java | 11 +- .../archunit/library/dependencies/Slice.java | 13 ++ .../library/dependencies/SliceAssignment.java | 35 ++++ .../library/dependencies/SliceIdentifier.java | 90 +++++++++ .../archunit/library/dependencies/Slices.java | 190 +++++++++++------- .../dependencies/SlicesRuleDefinition.java | 34 ++++ .../dependencies/SliceIdentifierTest.java | 47 +++++ .../library/dependencies/SlicesTest.java | 72 +++++++ 11 files changed, 501 insertions(+), 79 deletions(-) create mode 100644 archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceAssignment.java create mode 100644 archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceIdentifier.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceIdentifierTest.java diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/CyclicDependencyRulesTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/CyclicDependencyRulesTest.java index c6c28866f1..341d25af4c 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/CyclicDependencyRulesTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/CyclicDependencyRulesTest.java @@ -8,6 +8,8 @@ import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.ArchUnitRunner; import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.dependencies.SliceAssignment; +import com.tngtech.archunit.library.dependencies.SliceIdentifier; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @@ -54,4 +56,30 @@ public class CyclicDependencyRulesTest { .should().beFreeOfCycles() .ignoreDependency(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class, ClassCallingConstructorInSliceFive.class) .ignoreDependency(resideInAPackage("..slice4.."), DescribedPredicate.alwaysTrue()); + + @ArchTest + public static final ArchRule no_cycles_in_freely_customized_slices = + slices().assignedFrom(inComplexSliceOneOrTwo()) + .namingSlices("$1[$2]") + .should().beFreeOfCycles(); + + private static SliceAssignment inComplexSliceOneOrTwo() { + return new SliceAssignment() { + @Override + public String getDescription() { + return "complex slice one or two"; + } + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + if (javaClass.getPackageName().contains("complexcycles.slice1")) { + return SliceIdentifier.of("Complex-Cycle", "One"); + } + if (javaClass.getPackageName().contains("complexcycles.slice2")) { + return SliceIdentifier.of("Complex-Cycle", "Two"); + } + return SliceIdentifier.ignore(); + } + }; + } } diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/CyclicDependencyRulesTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/CyclicDependencyRulesTest.java index 24876fb3ad..fcfe312b27 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/CyclicDependencyRulesTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/CyclicDependencyRulesTest.java @@ -1,11 +1,14 @@ package com.tngtech.archunit.exampletest.junit5; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.example.cycle.complexcycles.slice1.SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree; import com.tngtech.archunit.example.cycle.complexcycles.slice3.ClassCallingConstructorInSliceFive; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTag; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.dependencies.SliceAssignment; +import com.tngtech.archunit.library.dependencies.SliceIdentifier; import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; @@ -50,4 +53,30 @@ public class CyclicDependencyRulesTest { .should().beFreeOfCycles() .ignoreDependency(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class, ClassCallingConstructorInSliceFive.class) .ignoreDependency(resideInAPackage("..slice4.."), alwaysTrue()); + + @ArchTest + static final ArchRule no_cycles_in_freely_customized_slices = + slices().assignedFrom(inComplexSliceOneOrTwo()) + .namingSlices("$1[$2]") + .should().beFreeOfCycles(); + + private static SliceAssignment inComplexSliceOneOrTwo() { + return new SliceAssignment() { + @Override + public String getDescription() { + return "complex slice one or two"; + } + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + if (javaClass.getPackageName().contains("complexcycles.slice1")) { + return SliceIdentifier.of("Complex-Cycle", "One"); + } + if (javaClass.getPackageName().contains("complexcycles.slice2")) { + return SliceIdentifier.of("Complex-Cycle", "Two"); + } + return SliceIdentifier.ignore(); + } + }; + } } diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/CyclicDependencyRulesTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/CyclicDependencyRulesTest.java index 0c4b0834e4..7fe9a12312 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/CyclicDependencyRulesTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/CyclicDependencyRulesTest.java @@ -1,9 +1,12 @@ package com.tngtech.archunit.exampletest; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.example.cycle.complexcycles.slice1.SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree; import com.tngtech.archunit.example.cycle.complexcycles.slice3.ClassCallingConstructorInSliceFive; +import com.tngtech.archunit.library.dependencies.SliceAssignment; +import com.tngtech.archunit.library.dependencies.SliceIdentifier; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -74,4 +77,32 @@ public void no_cycles_in_complex_scenario_with_custom_ignore() { .ignoreDependency(resideInAPackage("..slice4.."), alwaysTrue()) .check(classes); } + + @Test + public void no_cycles_in_freely_customized_slices() { + slices().assignedFrom(inComplexSliceOneOrTwo()) + .namingSlices("$1[$2]") + .should().beFreeOfCycles() + .check(classes); + } + + private SliceAssignment inComplexSliceOneOrTwo() { + return new SliceAssignment() { + @Override + public String getDescription() { + return "complex slice one or two"; + } + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + if (javaClass.getPackageName().contains("complexcycles.slice1")) { + return SliceIdentifier.of("Complex-Cycle", "One"); + } + if (javaClass.getPackageName().contains("complexcycles.slice2")) { + return SliceIdentifier.of("Complex-Cycle", "Two"); + } + return SliceIdentifier.ignore(); + } + }; + } } 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 e8165ec7f1..662c620a76 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 @@ -443,12 +443,19 @@ Stream CyclicDependencyRulesTest() { .by(cycleFromComplexSlice1To2()) .by(cycleFromComplexSlice1To2To3To5()) + .ofRule("slices assigned from complex slice one or two should be free of cycles") + .by(cycleFromComplexSlice1To2("Complex-Cycle[One]", "Complex-Cycle[Two]")) + .toDynamicTests(); } private static CyclicErrorMatcher cycleFromComplexSlice1To2() { + return cycleFromComplexSlice1To2("slice1 of complexcycles", "slice2 of complexcycles"); + } + + private static CyclicErrorMatcher cycleFromComplexSlice1To2(String sliceOneDescription, String sliceTwoDescription) { return cycle() - .from("slice1 of complexcycles") + .from(sliceOneDescription) .by(callFromMethod(ClassOfMinimalCycleCallingSliceTwo.class, "callSliceTwo") .toMethod(ClassOfMinimalCycleCallingSliceOne.class, "callSliceOne") .inLine(9)) @@ -457,7 +464,7 @@ private static CyclicErrorMatcher cycleFromComplexSlice1To2() { .by(callFromMethod(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class, "callSliceTwo") .toConstructor(InstantiatedClassInSliceTwo.class) .inLine(10)) - .from("slice2 of complexcycles") + .from(sliceTwoDescription) .by(inheritanceFrom(SliceTwoInheritingFromSliceOne.class) .extending(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class)) .by(field(ClassOfMinimalCycleCallingSliceOne.class, "classInSliceOne") diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slice.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slice.java index c23bc88969..b2d4f86bb6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slice.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/Slice.java @@ -33,6 +33,19 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +/** + * A collection of {@link JavaClass JavaClasses} modelling some domain aspect of a code basis. This is conceptually + * a cut through a code base according to business logic. Take for example + *

+ * com.mycompany.myapp.order
+ * com.mycompany.myapp.customer
+ * com.mycompany.myapp.user
+ * com.mycompany.myapp.authorization
+ * 
+ * The top level packages under 'myapp' could be considered slices according to different domain aspects.
+ * Thus there could be a slice 'Order' housing all the classes from the {@code order} package, a slice 'Customer' + * housing all the classes from the {@code customer} package and so on. + */ public final class Slice extends ForwardingSet implements HasDescription, CanOverrideDescription { private final List matchingGroups; private Description description; diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceAssignment.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceAssignment.java new file mode 100644 index 0000000000..3592d9d090 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceAssignment.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.dependencies; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; + +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; + +/** + * A mapping {@link JavaClass} -> {@link SliceIdentifier} which defines how to partition + * a set of {@link JavaClasses} into {@link Slices}. All classes that are mapped to the same + * {@link SliceIdentifier} will belong to the same {@link Slice}.
+ * A {@link SliceAssignment} must provide a description to be used for the rule text of a rule + * formed via {@link SlicesRuleDefinition}. + */ +@PublicAPI(usage = INHERITANCE) +public interface SliceAssignment extends HasDescription { + SliceIdentifier getIdentifierOf(JavaClass javaClass); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceIdentifier.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceIdentifier.java new file mode 100644 index 0000000000..f780db6385 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SliceIdentifier.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.library.dependencies; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.google.common.collect.ImmutableList; +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +/** + * An unique identifier of a {@link Slice}. All {@link JavaClasses} that are assigned to the same + * {@link SliceIdentifier} are considered to belong to the same {@link Slice}.
+ * A {@link SliceIdentifier} consists of textual parts. Two {@link SliceIdentifier} are considered to + * be equal if and only if their parts are equal. The parts can also be referred to from + * {@link Slices#namingSlices(String)} via '{@code $x}' where '{@code x}' is the number of the part. + */ +public final class SliceIdentifier { + private final List parts; + + private SliceIdentifier(List parts) { + this.parts = ImmutableList.copyOf(parts); + } + + List getParts() { + return parts; + } + + @Override + public int hashCode() { + return Objects.hash(parts); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SliceIdentifier other = (SliceIdentifier) obj; + return Objects.equals(this.parts, other.parts); + } + + @Override + public String toString() { + return getClass().getSimpleName() + parts; + } + + @PublicAPI(usage = ACCESS) + public static SliceIdentifier of(String... parts) { + return of(ImmutableList.copyOf(parts)); + } + + @PublicAPI(usage = ACCESS) + public static SliceIdentifier of(List parts) { + checkNotNull(parts, "Supplied parts may not be null"); + checkArgument(!parts.isEmpty(), + "Parts of a %s must not be empty. Use %s.ignore() to ignore a %s", + SliceIdentifier.class.getSimpleName(), SliceIdentifier.class.getSimpleName(), JavaClass.class.getSimpleName()); + + return new SliceIdentifier(parts); + } + + @PublicAPI(usage = ACCESS) + public static SliceIdentifier ignore() { + return new SliceIdentifier(Collections.emptyList()); + } +} 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 e8402f6077..012671158c 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 @@ -16,6 +16,7 @@ package com.tngtech.archunit.library.dependencies; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -44,22 +45,8 @@ import static com.tngtech.archunit.core.domain.Dependency.toTargetClasses; /** - * Basic collection of {@link Slice} for tests on dependencies of package slices, e.g. to avoid cycles. - * Example to specify a {@link ClassesTransformer} to run {@link ArchRule ArchRules} against {@link Slices}: - *

- * Slices.matching("some.pkg.(*)..")
- * 
- * would group the packages - *
    - *
  • some.pkg.one.any
  • - *
  • some.pkg.one.other
  • - *
- * in the same slice, the package - *
    - *
  • some.pkg.two.any
  • - *
- * in a different slice.
- * The resulting {@link ClassesTransformer} can be used to specify an {@link ArchRule} on slices. + * Basic collection of {@link Slice} for tests of dependencies between different domain packages, e.g. to avoid cycles. + * Refer to {@link SlicesRuleDefinition} for further info on how to form an {@link ArchRule} to test slices. */ public final class Slices implements DescribedIterable, CanOverrideDescription { private final Iterable slices; @@ -93,11 +80,15 @@ public String getDescription() { * Allows the naming of single slices, where back references to the matching pattern can be denoted by '$' followed * by capturing group number.
* E.g. {@code namingSlices("Slice $1")} would name a slice matching {@code '*..service.(*)..*'} - * against {@code 'com.some.company.service.hello.something'} as 'Slice hello'. + * against {@code 'com.some.company.service.hello.something'} as 'Slice hello'.
+ * Likewise, if the slices were created by a {@link SliceAssignment} (compare + * {@link #assignedFrom(SliceAssignment)}), + * then the back reference refers to the n-th element of the identifier. * * @param pattern The naming pattern, e.g. 'Slice $1' * @return New (equivalent) slices with adjusted description for each single slice */ + @PublicAPI(usage = ACCESS) public Slices namingSlices(String pattern) { List newSlices = new ArrayList<>(); for (Slice slice : slices) { @@ -107,10 +98,63 @@ public Slices namingSlices(String pattern) { } /** - * @see Creator#matching(String) + * Supports partitioning a set of {@link JavaClasses} into different slices by matching the supplied + * package identifier. For identifier syntax, see {@link PackageMatcher}.
+ * The slicing is done according to capturing groups (thus if none are contained in the identifier, no more than + * a single slice will be the result). For example + *

+ * Suppose there are three classes:

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

+ * If slices are created by specifying

+ * {@code Slices.of(classes).byMatching("..slice.(*)..")}

+ * then the result will be two slices, the slice where the capturing group is 'one' and the slice where the + * capturing group is 'two'. + *

+ * + * @param packageIdentifier The identifier to match against + * @return Slices partitioned according the supplied package identifier */ + @PublicAPI(usage = ACCESS) public static Transformer matching(String packageIdentifier) { - return new Transformer(packageIdentifier, slicesMatchingDescription(packageIdentifier)); + PackageMatchingSliceIdentifier sliceIdentifier = new PackageMatchingSliceIdentifier(packageIdentifier); + String description = "slices matching " + sliceIdentifier.getDescription(); + return new Transformer(sliceIdentifier, description); + } + + /** + * Supports partitioning a set of {@link JavaClasses} into different {@link Slices} by the supplied + * {@link SliceAssignment}. This is basically a mapping {@link JavaClass} -> {@link SliceIdentifier}, + * i.e. if the {@link SliceAssignment} returns the same + * {@link SliceIdentifier} for two classes they will end up in the same slice. + * A {@link JavaClass} will be ignored within the slices, if its {@link SliceIdentifier} is + * {@link SliceIdentifier#ignore()}. For example + *

+ * Suppose there are four classes:

+ * {@code com.somewhere.SomeClass}
+ * {@code com.somewhere.AnotherClass}
+ * {@code com.other.elsewhere.YetAnotherClass}
+ * {@code com.randomly.anywhere.AndYetAnotherClass}

+ * If slices are created by specifying

+ * {@code Slices.of(classes).assignedFrom(customAssignment)}

+ * + * and the {@code customAssignment} maps

+ * + * {@code com.somewhere -> SliceIdentifier.of("somewhere")}
+ * {@code com.other.elsewhere -> SliceIdentifier.of("elsewhere")}
+ * {@code com.randomly -> SliceIdentifier.ignore()}

+ * then the result will be two slices, identified by the single strings 'somewhere' (containing {@code SomeClass} + * and {@code AnotherClass}) and 'elsewhere' (containing {@code YetAnotherClass}). The class {@code AndYetAnotherClass} + * will be missing from all slices. + * + * @param sliceAssignment The assignment of {@link JavaClass} to {@link SliceIdentifier} + * @return Slices partitioned according the supplied assignment + */ + @PublicAPI(usage = ACCESS) + public static Transformer assignedFrom(SliceAssignment sliceAssignment) { + String description = "slices assigned from " + sliceAssignment.getDescription(); + return new Transformer(sliceAssignment, description); } /** @@ -120,25 +164,25 @@ public static Transformer matching(String packageIdentifier) { * @see Slices */ public static class Transformer implements ClassesTransformer { - private final String packageIdentifier; + private final SliceAssignment sliceAssignment; private final String description; private final Optional namingPattern; private final SlicesPredicateAggregator predicate; - Transformer(String packageIdentifier, String description) { - this(packageIdentifier, description, new SlicesPredicateAggregator("that")); + Transformer(SliceAssignment sliceAssignment, String description) { + this(sliceAssignment, description, new SlicesPredicateAggregator("that")); } - private Transformer(String packageIdentifier, String description, SlicesPredicateAggregator predicate) { - this(packageIdentifier, description, Optional.absent(), predicate); + private Transformer(SliceAssignment sliceAssignment, String description, SlicesPredicateAggregator predicate) { + this(sliceAssignment, description, Optional.absent(), predicate); } - private Transformer(String packageIdentifier, + private Transformer(SliceAssignment sliceAssignment, String description, Optional namingPattern, SlicesPredicateAggregator predicate) { - this.packageIdentifier = checkNotNull(packageIdentifier); + this.sliceAssignment = checkNotNull(sliceAssignment); this.description = checkNotNull(description); this.namingPattern = checkNotNull(namingPattern); this.predicate = predicate; @@ -152,12 +196,12 @@ Transformer namingSlices(String pattern) { } private Transformer namingSlices(Optional pattern) { - return new Transformer(packageIdentifier, description, pattern, predicate); + return new Transformer(sliceAssignment, description, pattern, predicate); } @Override public Transformer as(String description) { - return new Transformer(packageIdentifier, description, predicate).namingSlices(namingPattern); + return new Transformer(sliceAssignment, description, predicate).namingSlices(namingPattern); } public Slices of(JavaClasses classes) { @@ -170,7 +214,7 @@ public Slices transform(Iterable dependencies) { @Override public Slices transform(JavaClasses classes) { - Slices slices = new Creator(classes).matching(packageIdentifier); + Slices slices = createSlices(classes); if (namingPattern.isPresent()) { slices = slices.namingSlices(namingPattern.get()); } @@ -180,10 +224,19 @@ public Slices transform(JavaClasses classes) { return slices.as(getDescription()); } + private Slices createSlices(JavaClasses classes) { + SliceBuilders sliceBuilders = new SliceBuilders(); + for (JavaClass clazz : classes) { + List identifierParts = sliceAssignment.getIdentifierOf(clazz).getParts(); + sliceBuilders.add(identifierParts, clazz); + } + return new Slices(sliceBuilders.build()); + } + @Override public Slices.Transformer that(final DescribedPredicate predicate) { String newDescription = this.predicate.joinDescription(getDescription(), predicate.getDescription()); - return new Transformer(packageIdentifier, newDescription, namingPattern, this.predicate.add(predicate)); + return new Transformer(sliceAssignment, newDescription, namingPattern, this.predicate.add(predicate)); } @Override @@ -192,11 +245,11 @@ public String getDescription() { } Transformer thatANDsPredicates() { - return new Transformer(packageIdentifier, description, namingPattern, predicate.thatANDs()); + return new Transformer(sliceAssignment, description, namingPattern, predicate.thatANDs()); } Transformer thatORsPredicates() { - return new Transformer(packageIdentifier, description, namingPattern, predicate.thatORs()); + return new Transformer(sliceAssignment, description, namingPattern, predicate.thatORs()); } } @@ -240,54 +293,12 @@ String joinDescription(String first, String second) { } } - public static final class Creator { - private final JavaClasses classes; - - private Creator(JavaClasses classes) { - this.classes = classes; - } - - /** - * Supports partitioning a set of {@link JavaClasses} into different slices by matching the supplied - * package identifier. For identifier syntax, see {@link PackageMatcher}.
- * The slicing is done according to capturing groups (thus if none are contained in the identifier, no more than - * a single slice will be the result). For example - *

- * Suppose there are three classes:

- * {@code com.example.slice.one.SomeClass}
- * {@code com.example.slice.one.AnotherClass}
- * {@code com.example.slice.two.YetAnotherClass}

- * If slices are created by specifying

- * {@code Slices.of(classes).byMatching("..slice.(*)..")}

- * then the result will be two slices, the slice where the capturing group is 'one' and the slice where the - * capturing group is 'two'. - *

- * - * @param packageIdentifier The identifier to match against - * @return Slices partitioned according the supplied package identifier - */ - @PublicAPI(usage = ACCESS) - public Slices matching(String packageIdentifier) { - SliceBuilders sliceBuilders = new SliceBuilders(); - PackageMatcher matcher = PackageMatcher.of(packageIdentifier); - for (JavaClass clazz : classes) { - Optional> groups = matcher.match(clazz.getPackageName()).transform(TO_GROUPS); - sliceBuilders.add(groups, clazz); - } - return new Slices(sliceBuilders.build()).as(slicesMatchingDescription(packageIdentifier)); - } - } - - private static String slicesMatchingDescription(String packageIdentifier) { - return String.format("slices matching '%s'", packageIdentifier); - } - private static class SliceBuilders { private final Map, Slice.Builder> sliceBuilders = new HashMap<>(); - void add(Optional> matchingGroups, JavaClass clazz) { - if (matchingGroups.isPresent()) { - put(matchingGroups.get(), clazz); + void add(List identifierParts, JavaClass clazz) { + if (!identifierParts.isEmpty()) { + put(identifierParts, clazz); } } @@ -306,4 +317,29 @@ Set build() { return result; } } + + private static class PackageMatchingSliceIdentifier implements SliceAssignment { + private final String packageIdentifier; + + private PackageMatchingSliceIdentifier(String packageIdentifier) { + this.packageIdentifier = checkNotNull(packageIdentifier); + } + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + PackageMatcher matcher = PackageMatcher.of(packageIdentifier); + Optional> result = matcher.match(javaClass.getPackageName()).transform(TO_GROUPS); + List parts = result.or(Collections.emptyList()); + return parts.isEmpty() ? SliceIdentifier.ignore() : SliceIdentifier.of(parts); + } + + @Override + public String getDescription() { + return slicesMatchingDescription(packageIdentifier); + } + + private static String slicesMatchingDescription(String packageIdentifier) { + return "'" + packageIdentifier + "'"; + } + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SlicesRuleDefinition.java b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SlicesRuleDefinition.java index cb4e5f5793..a976646dc5 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SlicesRuleDefinition.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/dependencies/SlicesRuleDefinition.java @@ -16,15 +16,38 @@ package com.tngtech.archunit.library.dependencies; import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.Priority; import com.tngtech.archunit.library.dependencies.syntax.GivenSlices; +import com.tngtech.archunit.library.dependencies.syntax.SlicesShould; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +/** + * Allows to specify {@link ArchRule ArchRules} for "slices" of a code base. A slice is conceptually + * a cut through a code base according to business logic. Take for example + *

+ * com.mycompany.myapp.order
+ * com.mycompany.myapp.customer
+ * com.mycompany.myapp.user
+ * com.mycompany.myapp.authorization
+ * 
+ * The top level packages under 'myapp' are composed according to different domain aspects. It is + * good practice, to keep such packages free of cycles, which is one capability that this class + * provides.
+ * Consider + *

+ * {@link #slices() slices()}.{@link Slices#matching(String) matching("..myapp.(*)..")}.{@link GivenSlices#should() should()}.{@link SlicesShould#beFreeOfCycles() beFreeOfCycles()}
+ * 
+ * Then this rule will assert, that the four slices of 'myapp' are free of cycles. + */ public final class SlicesRuleDefinition { private SlicesRuleDefinition() { } + /** + * Entry point into {@link SlicesRuleDefinition} + */ @PublicAPI(usage = ACCESS) public static Creator slices() { return new Creator(); @@ -34,9 +57,20 @@ public static class Creator { private Creator() { } + /** + * @see Slices#matching(String) + */ @PublicAPI(usage = ACCESS) public GivenSlices matching(String packageIdentifier) { return new GivenSlicesInternal(Priority.MEDIUM, Slices.matching(packageIdentifier)); } + + /** + * @see Slices#assignedFrom(SliceAssignment) + */ + @PublicAPI(usage = ACCESS) + public GivenSlices assignedFrom(SliceAssignment assignment) { + return new GivenSlicesInternal(Priority.MEDIUM, Slices.assignedFrom(assignment)); + } } } diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceIdentifierTest.java b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceIdentifierTest.java new file mode 100644 index 0000000000..f60eac2636 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SliceIdentifierTest.java @@ -0,0 +1,47 @@ +package com.tngtech.archunit.library.dependencies; + +import java.util.Collections; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class SliceIdentifierTest { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void rejects_null() { + thrown.expect(NullPointerException.class); + + SliceIdentifier.of((String[]) null); + } + + @Test + public void rejects_null_list() { + thrown.expect(NullPointerException.class); + + SliceIdentifier.of((List) null); + } + + @Test + public void rejects_empty_parts() { + expectEmptyPartsException(); + + SliceIdentifier.of(); + } + + @Test + public void rejects_empty_parts_list() { + expectEmptyPartsException(); + + SliceIdentifier.of(Collections.emptyList()); + } + + private void expectEmptyPartsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("empty"); + thrown.expectMessage("Use SliceIdentifier.ignore() to ignore a JavaClass"); + } +} \ No newline at end of file diff --git a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SlicesTest.java b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SlicesTest.java index f4dcaaeff4..f2a5b464a7 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SlicesTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/dependencies/SlicesTest.java @@ -1,5 +1,7 @@ package com.tngtech.archunit.library.dependencies; +import java.io.File; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -7,16 +9,21 @@ import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.base.DescribedIterable; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.base.Optional; 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.JavaMethod; import com.tngtech.archunit.core.domain.TestUtils; +import com.tngtech.archunit.testutil.Assertions; import org.junit.Test; import static com.google.common.collect.Iterables.getOnlyElement; import static com.tngtech.archunit.core.domain.TestUtils.dependencyFrom; import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext; import static com.tngtech.archunit.core.domain.TestUtils.simulateCall; +import static com.tngtech.archunit.testutil.Assertions.assertThatClasses; import static org.assertj.core.api.Assertions.assertThat; public class SlicesTest { @@ -30,6 +37,20 @@ public void matches_slices() { assertThat(Slices.matching("java.(*).(*)").transform(classes)).hasSize(1); } + @Test + public void matching_description() { + JavaClasses classes = importClassesWithContext(Object.class); + + Slices.Transformer transformer = Slices.matching("java.(*).."); + assertThat(transformer.getDescription()).isEqualTo("slices matching 'java.(*)..'"); + + Slices slices = transformer.transform(classes); + assertThat(slices.getDescription()).isEqualTo("slices matching 'java.(*)..'"); + + slices = transformer.that(DescribedPredicate.alwaysTrue().as("changed")).transform(classes); + assertThat(slices.getDescription()).isEqualTo("slices matching 'java.(*)..' that changed"); + } + @Test public void default_naming_slices() { JavaClasses classes = importClassesWithContext(Object.class, String.class, Pattern.class); @@ -69,4 +90,55 @@ public void slices_of_dependencies() { assertThat(slices).extractingResultOf("getDescription").containsOnly("Slice lang", "Slice util"); } + + @Test + public void slices_from_identifier() { + Slices slices = Slices.assignedFrom(assignmentOfJavaLangAndUtil("some description")) + .namingSlices("Any $1 - $2") + .transform(importClassesWithContext(Object.class, Number.class, List.class, Collection.class, File.class)); + + assertThat(slices.getDescription()).isEqualTo("slices assigned from some description"); + assertThat(slices).extractingResultOf("getDescription").containsOnly("Any Lang - $2", "Any Adjusted - Util"); + assertThat(slices).hasSize(2); + assertThatClasses(getSliceOf(Object.class, slices)).contain(Number.class); + assertThatClasses(getSliceOf(List.class, slices)).contain(Collection.class); + Assertions.assertThat(tryGetSliceOf(File.class, slices)) + .as("Slice of class java.io.File (which should be missing from the assignment)") + .isAbsent(); + } + + private Slice getSliceOf(Class clazz, Slices slices) { + return tryGetSliceOf(clazz, slices).get(); + } + + private Optional tryGetSliceOf(Class clazz, Slices slices) { + for (Slice slice : slices) { + for (JavaClass javaClass : slice) { + if (javaClass.isEquivalentTo(clazz)) { + return Optional.of(slice); + } + } + } + return Optional.absent(); + } + + private SliceAssignment assignmentOfJavaLangAndUtil(final String description) { + return new SliceAssignment() { + @Override + public String getDescription() { + return description; + } + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + if (javaClass.getPackageName().startsWith("java.lang")) { + return SliceIdentifier.of("Lang"); + } + if (javaClass.getPackageName().startsWith("java.util")) { + return SliceIdentifier.of("Adjusted", "Util"); + } + return SliceIdentifier.ignore(); + } + }; + } } \ No newline at end of file