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 914ba5cac8..7318c8951f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java +++ b/archunit/src/main/java/com/tngtech/archunit/library/Architectures.java @@ -766,6 +766,7 @@ public static final class OnionArchitecture implements ArchRule { private Map> adapterPredicates = new LinkedHashMap<>(); private boolean optionalLayers = false; private List ignoredDependencies = new ArrayList<>(); + private AllClassesAreContainedInArchitectureCheck allClassesAreContainedInArchitectureCheck = new AllClassesAreContainedInArchitectureCheck.Disabled(); private OnionArchitecture() { overriddenDescription = Optional.empty(); @@ -856,6 +857,46 @@ public OnionArchitecture ignoreDependency(DescribedPredicate return this; } + /** + * Ensure that all classes under test are contained within a defined onion architecture component. + * + * @see #ensureAllClassesAreContainedInArchitectureIgnoring(String...) + * @see #ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate) + */ + @PublicAPI(usage = ACCESS) + public OnionArchitecture ensureAllClassesAreContainedInArchitecture() { + return ensureAllClassesAreContainedInArchitectureIgnoring(alwaysFalse()); + } + + /** + * Like {@link #ensureAllClassesAreContainedInArchitecture()} but will ignore classes in packages matching + * the specified {@link PackageMatcher packageIdentifiers}. + * + * @param packageIdentifiers {@link PackageMatcher packageIdentifiers} specifying which classes may live outside the architecture + * + * @see #ensureAllClassesAreContainedInArchitecture() + * @see #ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate) + */ + @PublicAPI(usage = ACCESS) + public OnionArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(String... packageIdentifiers) { + return ensureAllClassesAreContainedInArchitectureIgnoring(resideInAnyPackage(packageIdentifiers)); + } + + /** + * Like {@link #ensureAllClassesAreContainedInArchitecture()} but will ignore classes in packages matching + * the specified {@link DescribedPredicate predicate}. + * + * @param predicate {@link DescribedPredicate predicate} specifying which classes may live outside the architecture + * + * @see #ensureAllClassesAreContainedInArchitecture() + * @see #ensureAllClassesAreContainedInArchitectureIgnoring(String...) + */ + @PublicAPI(usage = ACCESS) + public OnionArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate predicate) { + allClassesAreContainedInArchitectureCheck = new AllClassesAreContainedInArchitectureCheck.Enabled(predicate); + return this; + } + private DescribedPredicate byPackagePredicate(String[] packageIdentifiers) { return resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers)); } @@ -877,9 +918,13 @@ private LayeredArchitecture layeredArchitectureDelegate() { .layer(adapterLayer).definedBy(adapter.getValue()) .whereLayer(adapterLayer).mayNotBeAccessedByAnyLayer(); } + for (IgnoredDependency ignoredDependency : this.ignoredDependencies) { layeredArchitectureDelegate = ignoredDependency.ignoreFor(layeredArchitectureDelegate); } + + layeredArchitectureDelegate = allClassesAreContainedInArchitectureCheck.configure(layeredArchitectureDelegate); + return layeredArchitectureDelegate.as(getDescription()); } @@ -955,5 +1000,29 @@ LayeredArchitecture ignoreFor(LayeredArchitecture layeredArchitecture) { return layeredArchitecture.ignoreDependency(origin, target); } } + + private abstract static class AllClassesAreContainedInArchitectureCheck { + abstract LayeredArchitecture configure(LayeredArchitecture layeredArchitecture); + + static class Enabled extends AllClassesAreContainedInArchitectureCheck { + private final DescribedPredicate ignorePredicate; + + private Enabled(DescribedPredicate ignorePredicate) { + this.ignorePredicate = ignorePredicate; + } + + @Override + LayeredArchitecture configure(LayeredArchitecture layeredArchitecture) { + return layeredArchitecture.ensureAllClassesAreContainedInArchitectureIgnoring(ignorePredicate); + } + } + + static class Disabled extends AllClassesAreContainedInArchitectureCheck { + @Override + LayeredArchitecture configure(LayeredArchitecture layeredArchitecture) { + return layeredArchitecture; + } + } + } } } 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 0b3b4ed93e..16659338a4 100644 --- a/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/library/OnionArchitectureTest.java @@ -9,6 +9,9 @@ import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.EvaluationResult; import com.tngtech.archunit.library.Architectures.OnionArchitecture; +import com.tngtech.archunit.library.testclasses.coveringallclasses.first.First; +import com.tngtech.archunit.library.testclasses.coveringallclasses.second.Second; +import com.tngtech.archunit.library.testclasses.coveringallclasses.third.Third; import com.tngtech.archunit.library.testclasses.onionarchitecture.adapter.cli.CliAdapterLayerClass; import com.tngtech.archunit.library.testclasses.onionarchitecture.adapter.persistence.PersistenceAdapterLayerClass; import com.tngtech.archunit.library.testclasses.onionarchitecture.adapter.rest.RestAdapterLayerClass; @@ -24,6 +27,7 @@ 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.simpleName; 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; @@ -32,6 +36,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.archunit.testutil.Assertions.assertThatRule; import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; import static java.beans.Introspector.decapitalize; import static java.lang.System.lineSeparator; @@ -195,6 +200,46 @@ public void onion_architecture_rejects_empty_layers_if_layers_are_explicitly_not assertFailureOnionArchitectureWithEmptyLayers(result); } + @Test + public void onion_architecture_ensure_all_classes_are_contained_in_architecture() { + JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class); + + OnionArchitecture architectureNotCoveringAllClasses = onionArchitecture().withOptionalLayers(true) + .domainModels("..first..") + .ensureAllClassesAreContainedInArchitecture(); + + assertThatRule(architectureNotCoveringAllClasses).checking(classes) + .hasOnlyOneViolation("Class <" + Second.class.getName() + "> is not contained in architecture"); + + OnionArchitecture architectureCoveringAllClasses = architectureNotCoveringAllClasses + .domainServices("..second.."); + assertThatRule(architectureCoveringAllClasses).checking(classes).hasNoViolation(); + } + + @Test + public void onion_architecture_ensure_all_classes_are_contained_in_architecture_ignoring_packages() { + JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class, Third.class); + + OnionArchitecture architecture = onionArchitecture().withOptionalLayers(true) + .domainModels("..first..") + .ensureAllClassesAreContainedInArchitectureIgnoring("..second.."); + + assertThatRule(architecture).checking(classes) + .hasOnlyOneViolation("Class <" + Third.class.getName() + "> is not contained in architecture"); + } + + @Test + public void onion_architecture_ensure_all_classes_are_contained_in_architecture_ignoring_predicate() { + JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class, Third.class); + + OnionArchitecture architecture = onionArchitecture().withOptionalLayers(true) + .domainModels("..first..") + .ensureAllClassesAreContainedInArchitectureIgnoring(simpleName("Second")); + + assertThatRule(architecture).checking(classes) + .hasOnlyOneViolation("Class <" + Third.class.getName() + "> is not contained in architecture"); + } + private static OnionArchitecture getTestOnionArchitecture() { return onionArchitecture() .domainModels(absolute("onionarchitecture.domain.model"))