Skip to content

Commit

Permalink
Add ensureAllClassesAreContainedInLayers() to Architectures #278
Browse files Browse the repository at this point in the history
This will add possibilities to ensure that all classes under test are contained within the respective `LayeredArchitecture`/`OnionArchitecture`. It will help users to make sure they don't overlook any classes when defining their architectures. Furthermore, it will help with maintainability of the architectures when new classes are added to the code base later on. In detail the following methods have been added to `LayeredArchitecture` and `OnionArchitecture`:

* `ensureAllClassesAreContainedInArchitecture()`
* `ensureAllClassesAreContainedInArchitectureIgnoring(packageIdentifiers)`
* `ensureAllClassesAreContainedInArchitectureIgnoring(predicate)`

Resolves #222
  • Loading branch information
codecholeric authored Jul 2, 2022
2 parents 5b1b7c3 + 3352e33 commit 65843a8
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 5 deletions.
171 changes: 166 additions & 5 deletions archunit/src/main/java/com/tngtech/archunit/library/Architectures.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,28 +119,32 @@ public static final class LayeredArchitecture implements ArchRule {
private final PredicateAggregator<Dependency> irrelevantDependenciesPredicate;
private final Optional<String> overriddenDescription;
private final boolean optionalLayers;
private final AllClassesAreContainedInArchitectureCheck allClassesAreContainedInArchitectureCheck;

private LayeredArchitecture(DependencySettings dependencySettings) {
this(new LayerDefinitions(),
new LinkedHashSet<>(),
dependencySettings,
new PredicateAggregator<Dependency>().thatORs(),
Optional.empty(),
false);
false,
new AllClassesAreContainedInArchitectureCheck.Disabled());
}

private LayeredArchitecture(LayerDefinitions layerDefinitions,
Set<LayerDependencySpecification> dependencySpecifications,
DependencySettings dependencySettings,
PredicateAggregator<Dependency> irrelevantDependenciesPredicate,
Optional<String> overriddenDescription,
boolean optionalLayers) {
boolean optionalLayers,
AllClassesAreContainedInArchitectureCheck allClassesAreContainedInArchitectureCheck) {
this.layerDefinitions = layerDefinitions;
this.dependencySpecifications = dependencySpecifications;
this.dependencySettings = dependencySettings;
this.irrelevantDependenciesPredicate = irrelevantDependenciesPredicate;
this.overriddenDescription = overriddenDescription;
this.optionalLayers = optionalLayers;
this.allClassesAreContainedInArchitectureCheck = allClassesAreContainedInArchitectureCheck;
}

/**
Expand All @@ -158,7 +162,8 @@ public LayeredArchitecture withOptionalLayers(boolean optionalLayers) {
dependencySettings,
irrelevantDependenciesPredicate,
overriddenDescription,
optionalLayers
optionalLayers,
allClassesAreContainedInArchitectureCheck
);
}

Expand Down Expand Up @@ -223,6 +228,8 @@ public String toString() {
public EvaluationResult evaluate(JavaClasses classes) {
EvaluationResult result = new EvaluationResult(this, Priority.MEDIUM);
checkEmptyLayers(classes, result);
allClassesAreContainedInArchitectureCheck.evaluate(classes, layerDefinitions).ifPresent(result::add);

for (LayerDependencySpecification specification : dependencySpecifications) {
result.add(evaluateDependenciesShouldBeSatisfied(classes, specification));
}
Expand All @@ -239,6 +246,54 @@ private void checkEmptyLayers(JavaClasses classes, EvaluationResult result) {
}
}

/**
* Ensure that all classes under test are contained within a defined layer of the architecture.
*
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(String...)
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate)
*/
@PublicAPI(usage = ACCESS)
public LayeredArchitecture 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 LayeredArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(String... packageIdentifiers) {
return ensureAllClassesAreContainedInArchitectureIgnoring(
resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(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 LayeredArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate<? super JavaClass> predicate) {
return new LayeredArchitecture(
layerDefinitions,
dependencySpecifications,
dependencySettings,
irrelevantDependenciesPredicate,
overriddenDescription,
optionalLayers,
new AllClassesAreContainedInArchitectureCheck.Enabled(predicate)
);
}

private EvaluationResult evaluateLayersShouldNotBeEmpty(JavaClasses classes, LayerDefinition layerDefinition) {
return classes().that(layerDefinitions.containsPredicateFor(layerDefinition.name))
.should(notBeEmptyFor(layerDefinition))
Expand Down Expand Up @@ -310,7 +365,8 @@ public LayeredArchitecture as(String newDescription) {
dependencySettings,
irrelevantDependenciesPredicate,
Optional.of(newDescription),
optionalLayers
optionalLayers,
allClassesAreContainedInArchitectureCheck
);
}

Expand Down Expand Up @@ -347,7 +403,8 @@ public LayeredArchitecture ignoreDependency(
dependencySettings,
irrelevantDependenciesPredicate.add(dependency(origin, target)),
overriddenDescription,
optionalLayers
optionalLayers,
allClassesAreContainedInArchitectureCheck
);
}

Expand All @@ -369,6 +426,41 @@ private void checkLayerNamesExist(String... layerNames) {
}
}

private abstract static class AllClassesAreContainedInArchitectureCheck {
abstract Optional<EvaluationResult> evaluate(final JavaClasses classes, final LayerDefinitions layerDefinitions);

static class Enabled extends AllClassesAreContainedInArchitectureCheck {
private final DescribedPredicate<? super JavaClass> ignorePredicate;

private Enabled(DescribedPredicate<? super JavaClass> ignorePredicate) {
this.ignorePredicate = ignorePredicate;
}

Optional<EvaluationResult> evaluate(final JavaClasses classes, final LayerDefinitions layerDefinitions) {
return Optional.of(classes().should(beContainedInLayers(layerDefinitions)).evaluate(classes));
}

private ArchCondition<JavaClass> beContainedInLayers(LayerDefinitions layerDefinitions) {
DescribedPredicate<JavaClass> classContainedInLayers = layerDefinitions.containsPredicateForAll();
return new ArchCondition<JavaClass>("be contained in architecture") {
@Override
public void check(JavaClass javaClass, ConditionEvents events) {
if (!ignorePredicate.test(javaClass) && !classContainedInLayers.test(javaClass)) {
events.add(violated(this, String.format("Class <%s> is not contained in architecture", javaClass.getName())));
}
}
};
}
}

static class Disabled extends AllClassesAreContainedInArchitectureCheck {
@Override
Optional<EvaluationResult> evaluate(JavaClasses classes, LayerDefinitions layerDefinitions) {
return Optional.empty();
}
}
}

private static ArchCondition<JavaClass> notBeEmptyFor(final LayeredArchitecture.LayerDefinition layerDefinition) {
return new LayerShouldNotBeEmptyCondition(layerDefinition);
}
Expand Down Expand Up @@ -674,6 +766,7 @@ public static final class OnionArchitecture implements ArchRule {
private Map<String, DescribedPredicate<? super JavaClass>> adapterPredicates = new LinkedHashMap<>();
private boolean optionalLayers = false;
private List<IgnoredDependency> ignoredDependencies = new ArrayList<>();
private AllClassesAreContainedInArchitectureCheck allClassesAreContainedInArchitectureCheck = new AllClassesAreContainedInArchitectureCheck.Disabled();

private OnionArchitecture() {
overriddenDescription = Optional.empty();
Expand Down Expand Up @@ -764,6 +857,46 @@ public OnionArchitecture ignoreDependency(DescribedPredicate<? super JavaClass>
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<? super JavaClass> predicate) {
allClassesAreContainedInArchitectureCheck = new AllClassesAreContainedInArchitectureCheck.Enabled(predicate);
return this;
}

private DescribedPredicate<JavaClass> byPackagePredicate(String[] packageIdentifiers) {
return resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers));
}
Expand All @@ -785,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());
}

Expand Down Expand Up @@ -863,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<? super JavaClass> ignorePredicate;

private Enabled(DescribedPredicate<? super JavaClass> 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;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.library.Architectures.LayeredArchitecture;
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.dependencysettings.DependencySettingsOutsideOfLayersAccessingLayers;
import com.tngtech.archunit.library.testclasses.dependencysettings.forbidden_backwards.DependencySettingsForbiddenByMayOnlyBeAccessed;
import com.tngtech.archunit.library.testclasses.dependencysettings.forbidden_forwards.DependencySettingsForbiddenByMayOnlyAccess;
Expand All @@ -34,6 +37,7 @@
import org.junit.runner.RunWith;

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.JavaConstructor.CONSTRUCTOR_NAME;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
Expand Down Expand Up @@ -379,6 +383,46 @@ public void layered_architecture_supports_dependency_setting_considering_only_de
assertPatternMatches(result.getFailureReport().getDetails(), dependencySettingsViolationsInLayers());
}

@Test
public void layered_architecture_ensure_all_classes_are_contained_in_architecture() {
JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class);

LayeredArchitecture architectureNotCoveringAllClasses = layeredArchitecture().consideringAllDependencies()
.layer("One").definedBy("..first..")
.ensureAllClassesAreContainedInArchitecture();

assertThatRule(architectureNotCoveringAllClasses).checking(classes)
.hasOnlyOneViolation("Class <" + Second.class.getName() + "> is not contained in architecture");

LayeredArchitecture architectureCoveringAllClasses = architectureNotCoveringAllClasses
.layer("Two").definedBy("..second..");
assertThatRule(architectureCoveringAllClasses).checking(classes).hasNoViolation();
}

@Test
public void layered_architecture_ensure_all_classes_are_contained_in_architecture_ignoring_packages() {
JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class, Third.class);

LayeredArchitecture architecture = layeredArchitecture().consideringAllDependencies()
.layer("One").definedBy("..first..")
.ensureAllClassesAreContainedInArchitectureIgnoring("..second..");

assertThatRule(architecture).checking(classes)
.hasOnlyOneViolation("Class <" + Third.class.getName() + "> is not contained in architecture");
}

@Test
public void layered_architecture_ensure_all_classes_are_contained_in_architecture_ignoring_predicate() {
JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class, Third.class);

LayeredArchitecture architecture = layeredArchitecture().consideringAllDependencies()
.layer("One").definedBy("..first..")
.ensureAllClassesAreContainedInArchitectureIgnoring(simpleName("Second"));

assertThatRule(architecture).checking(classes)
.hasOnlyOneViolation("Class <" + Third.class.getName() + "> is not contained in architecture");
}

private LayeredArchitecture defineLayeredArchitectureForDependencySettings(LayeredArchitecture layeredArchitecture) {
return layeredArchitecture
.layer("Origin").definedBy("..library.testclasses.dependencysettings.origin..")
Expand Down
Loading

0 comments on commit 65843a8

Please sign in to comment.